diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 5492602a5..610b08a1d 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -114,11 +114,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { .build() .expect("failed to create client"); - let auth_dir = dirs::config_local_dir() - .ok_or_else(|| anyhow::anyhow!("could not determine cache directory for current platform"))? - .join("rattler/auth"); - - let authentication_storage = AuthenticationStorage::new("rattler_credentials", &auth_dir); + let authentication_storage = AuthenticationStorage::default(); let download_client = AuthenticatedClient::from_client(download_client, authentication_storage); let multi_progress = global_multi_progress(); diff --git a/crates/rattler_networking/src/authentication_storage/fallback_storage.rs b/crates/rattler_networking/src/authentication_storage/backends/file.rs similarity index 53% rename from crates/rattler_networking/src/authentication_storage/fallback_storage.rs rename to crates/rattler_networking/src/authentication_storage/backends/file.rs index e792a9734..2d249e83b 100644 --- a/crates/rattler_networking/src/authentication_storage/fallback_storage.rs +++ b/crates/rattler_networking/src/authentication_storage/backends/file.rs @@ -1,26 +1,30 @@ -//! Fallback storage for passwords. +//! file storage for passwords. +use anyhow::Result; use fslock::LockFile; use once_cell::sync::Lazy; use std::collections::{HashMap, HashSet}; use std::{path::PathBuf, sync::Mutex}; +use crate::authentication_storage::StorageBackend; +use crate::Authentication; + /// A struct that implements storage and access of authentication /// information backed by a on-disk JSON file -#[derive(Clone)] -pub struct FallbackStorage { +#[derive(Clone, Debug)] +pub struct FileStorage { /// The path to the JSON file pub path: PathBuf, } -/// An error that can occur when accessing the fallback storage +/// An error that can occur when accessing the file storage #[derive(thiserror::Error, Debug)] -pub enum FallbackStorageError { - /// An IO error occurred when accessing the fallback storage +pub enum FileStorageError { + /// An IO error occurred when accessing the file storage #[error("IO error: {0}")] IOError(#[from] std::io::Error), - /// Failed to lock the fallback storage file - #[error("failed to lock fallback storage file {0}.")] + /// Failed to lock the file storage file + #[error("failed to lock file storage file {0}.")] FailedToLock(String, #[source] std::io::Error), /// An error occurred when (de)serializing the credentials @@ -28,67 +32,44 @@ pub enum FallbackStorageError { JSONError(#[from] serde_json::Error), } -impl FallbackStorage { - /// Create a new fallback storage with the given path +impl FileStorage { + /// Create a new file storage with the given path pub fn new(path: PathBuf) -> Self { Self { path } } - /// Lock the fallback storage file for reading and writing. This will block until the lock is + /// Lock the file storage file for reading and writing. This will block until the lock is /// acquired. - fn lock(&self) -> Result { + fn lock(&self) -> Result { std::fs::create_dir_all(self.path.parent().unwrap())?; let path = self.path.with_extension("lock"); - let mut lock = fslock::LockFile::open(&path).map_err(|e| { - FallbackStorageError::FailedToLock(path.to_string_lossy().into_owned(), e) - })?; + let mut lock = fslock::LockFile::open(&path) + .map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))?; // First try to lock the file without block. If we can't immediately get the lock we block and issue a debug message. - if !lock.try_lock_with_pid().map_err(|e| { - FallbackStorageError::FailedToLock(path.to_string_lossy().into_owned(), e) - })? { + if !lock + .try_lock_with_pid() + .map_err(|e| FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e))? + { tracing::debug!("waiting for lock on {}", path.to_string_lossy()); lock.lock_with_pid().map_err(|e| { - FallbackStorageError::FailedToLock(path.to_string_lossy().into_owned(), e) + FileStorageError::FailedToLock(path.to_string_lossy().into_owned(), e) })?; } Ok(lock) } - /// Store the given authentication information for the given host - pub fn set_password(&self, host: &str, password: &str) -> Result<(), FallbackStorageError> { - let _lock = self.lock()?; - let mut dict = self.read_json()?; - dict.insert(host.to_string(), password.to_string()); - self.write_json(&dict) - } - - /// Retrieve the authentication information for the given host - pub fn get_password(&self, host: &str) -> Result, FallbackStorageError> { - let _lock = self.lock()?; - let dict = self.read_json()?; - Ok(dict.get(host).cloned()) - } - - /// Delete the authentication information for the given host - pub fn delete_password(&self, host: &str) -> Result<(), FallbackStorageError> { - let _lock = self.lock()?; - let mut dict = self.read_json()?; - dict.remove(host); - self.write_json(&dict) - } - /// Read the JSON file and deserialize it into a HashMap, or return an empty HashMap if the file /// does not exist - fn read_json(&self) -> Result, FallbackStorageError> { + fn read_json(&self) -> Result, FileStorageError> { if !self.path.exists() { static WARN_GUARD: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); let mut guard = WARN_GUARD.lock().unwrap(); if !guard.insert(self.path.clone()) { tracing::warn!( - "Can't find path for fallback storage on {}", + "Can't find path for file storage on {}", self.path.to_string_lossy() ); } @@ -101,7 +82,7 @@ impl FallbackStorage { } /// Serialize the given HashMap and write it to the JSON file - fn write_json(&self, dict: &HashMap) -> Result<(), FallbackStorageError> { + fn write_json(&self, dict: &HashMap) -> Result<(), FileStorageError> { let file = std::fs::File::create(&self.path)?; let writer = std::io::BufWriter::new(file); serde_json::to_writer(writer, dict)?; @@ -109,6 +90,37 @@ impl FallbackStorage { } } +impl StorageBackend for FileStorage { + fn store(&self, host: &str, authentication: &crate::Authentication) -> Result<()> { + let _lock = self.lock()?; + let mut dict = self.read_json()?; + dict.insert(host.to_string(), authentication.clone()); + Ok(self.write_json(&dict)?) + } + + fn get(&self, host: &str) -> Result> { + let _lock = self.lock()?; + let dict = self.read_json()?; + Ok(dict.get(host).cloned()) + } + + fn delete(&self, host: &str) -> Result<()> { + let _lock = self.lock()?; + let mut dict = self.read_json()?; + dict.remove(host); + Ok(self.write_json(&dict)?) + } +} + +impl Default for FileStorage { + fn default() -> Self { + let mut path = dirs::home_dir().unwrap(); + path.push(".rattler"); + path.push("credentials.json"); + Self { path } + } +} + #[cfg(test)] mod tests { use super::*; @@ -116,25 +128,27 @@ mod tests { use tempfile::tempdir; #[test] - fn test_fallback_storage() { + fn test_file_storage() { let file = tempdir().unwrap(); let path = file.path().join("test.json"); - let storage = FallbackStorage::new(path.clone()); + let storage = FileStorage::new(path.clone()); - assert_eq!(storage.get_password("test").unwrap(), None); + assert_eq!(storage.get("test").unwrap(), None); - storage.set_password("test", "password").unwrap(); + storage + .store("test", &Authentication::CondaToken("password".to_string())) + .unwrap(); assert_eq!( - storage.get_password("test").unwrap(), - Some("password".to_string()) + storage.get("test").unwrap(), + Some(Authentication::CondaToken("password".to_string())) ); - storage.delete_password("test").unwrap(); - assert_eq!(storage.get_password("test").unwrap(), None); + storage.delete("test").unwrap(); + assert_eq!(storage.get("test").unwrap(), None); let mut file = std::fs::File::create(&path).unwrap(); file.write_all(b"invalid json").unwrap(); - assert!(storage.get_password("test").is_err()); + assert!(storage.get("test").is_err()); } } diff --git a/crates/rattler_networking/src/authentication_storage/backends/keyring.rs b/crates/rattler_networking/src/authentication_storage/backends/keyring.rs new file mode 100644 index 000000000..e2a3df13c --- /dev/null +++ b/crates/rattler_networking/src/authentication_storage/backends/keyring.rs @@ -0,0 +1,88 @@ +//! Backend to store credentials in the operating system's keyring + +use anyhow::Result; +use keyring::Entry; +use std::str::FromStr; + +use crate::{authentication_storage::StorageBackend, Authentication}; + +#[derive(Clone, Debug)] +/// A storage backend that stores credentials in the operating system's keyring +pub struct KeyringAuthenticationStorage { + /// The store_key needs to be unique per program as it is stored + /// in a global dictionary in the operating system + pub store_key: String, +} + +impl KeyringAuthenticationStorage { + /// Create a new authentication storage with the given store key + pub fn from_key(store_key: &str) -> Self { + Self { + store_key: store_key.to_string(), + } + } +} + +/// An error that can occur when accessing the authentication storage +#[derive(thiserror::Error, Debug)] +pub enum KeyringAuthenticationStorageError { + /// An error occurred when accessing the authentication storage + #[error("Could not retrieve credentials from authentication storage: {0}")] + StorageError(#[from] keyring::Error), + + /// An error occurred when serializing the credentials + #[error("Could not serialize credentials {0}")] + SerializeCredentialsError(#[from] serde_json::Error), + + /// An error occurred when parsing the credentials + #[error("Could not parse credentials stored for {host}")] + ParseCredentialsError { + /// The host for which the credentials could not be parsed + host: String, + }, +} + +impl Default for KeyringAuthenticationStorage { + fn default() -> Self { + Self::from_key("rattler") + } +} + +impl StorageBackend for KeyringAuthenticationStorage { + fn store(&self, host: &str, authentication: &Authentication) -> Result<()> { + let password = serde_json::to_string(authentication)?; + let entry = Entry::new(&self.store_key, host)?; + + entry.set_password(&password)?; + + Ok(()) + } + + fn get(&self, host: &str) -> Result> { + let entry = Entry::new(&self.store_key, host)?; + let password = entry.get_password(); + + let p_string = match password { + Ok(password) => password, + Err(_) => return Ok(None), + }; + + match Authentication::from_str(&p_string) { + Ok(auth) => Ok(Some(auth)), + Err(err) => { + tracing::warn!("Error parsing credentials for {}: {:?}", host, err); + Err(KeyringAuthenticationStorageError::ParseCredentialsError { + host: host.to_string(), + } + .into()) + } + } + } + + fn delete(&self, host: &str) -> Result<()> { + let entry = Entry::new(&self.store_key, host)?; + entry.delete_password()?; + + Ok(()) + } +} diff --git a/crates/rattler_networking/src/authentication_storage/backends/mod.rs b/crates/rattler_networking/src/authentication_storage/backends/mod.rs new file mode 100644 index 000000000..2d509b202 --- /dev/null +++ b/crates/rattler_networking/src/authentication_storage/backends/mod.rs @@ -0,0 +1,4 @@ +//! Multiple backends for storing authentication data. + +pub mod file; +pub mod keyring; diff --git a/crates/rattler_networking/src/authentication_storage/mod.rs b/crates/rattler_networking/src/authentication_storage/mod.rs index d63fbd225..1b1207cc4 100644 --- a/crates/rattler_networking/src/authentication_storage/mod.rs +++ b/crates/rattler_networking/src/authentication_storage/mod.rs @@ -1,4 +1,19 @@ -//! This module provides a way to store and retrieve authentication information for a given host. +//! This module contains the authentication storage backend trait and implementations +use self::authentication::Authentication; +use anyhow::Result; + pub mod authentication; -pub mod fallback_storage; +pub mod backends; pub mod storage; + +/// A trait that defines the interface for authentication storage backends +pub trait StorageBackend: std::fmt::Debug { + /// Store the given authentication information for the given host + fn store(&self, host: &str, authentication: &Authentication) -> Result<()>; + + /// Retrieve the authentication information for the given host + fn get(&self, host: &str) -> Result>; + + /// Delete the authentication information for the given host + fn delete(&self, host: &str) -> Result<()>; +} diff --git a/crates/rattler_networking/src/authentication_storage/storage.rs b/crates/rattler_networking/src/authentication_storage/storage.rs index 98e6a31c9..ee6f4d507 100644 --- a/crates/rattler_networking/src/authentication_storage/storage.rs +++ b/crates/rattler_networking/src/authentication_storage/storage.rs @@ -1,94 +1,76 @@ //! Storage and access of authentication information + +use anyhow::{anyhow, Result}; +use reqwest::IntoUrl; use std::{ collections::HashMap, - path::Path, - str::FromStr, sync::{Arc, Mutex}, }; +use url::Url; -use keyring::Entry; -use reqwest::{IntoUrl, Url}; - -use super::{authentication::Authentication, fallback_storage}; +use super::{ + authentication::Authentication, + backends::{file::FileStorage, keyring::KeyringAuthenticationStorage}, + StorageBackend, +}; -/// A struct that implements storage and access of authentication -/// information -#[derive(Clone)] +#[derive(Debug, Clone)] +/// This struct implements storage and access of authentication +/// information backed by multiple storage backends +/// (e.g. keyring and file storage) +/// Credentials are stored and retrieved from the backends in the +/// order they are added to the storage pub struct AuthenticationStorage { - /// The store_key needs to be unique per program as it is stored - /// in a global dictionary in the operating system - pub store_key: String, + backends: Vec>, + cache: Arc>>>, +} - /// Fallback Storage that will be used if the is no key store application available. - pub fallback_storage: fallback_storage::FallbackStorage, +impl Default for AuthenticationStorage { + fn default() -> Self { + let mut storage = Self::new(); - /// A cache so that we don't have to access the keyring all the time - cache: Arc>>>, + storage.add_backend(Arc::from(KeyringAuthenticationStorage::default())); + storage.add_backend(Arc::from(FileStorage::default())); + + storage + } } impl AuthenticationStorage { - /// Create a new authentication storage with the given store key - pub fn new(store_key: &str, fallback_folder: &Path) -> AuthenticationStorage { - let fallback_location = fallback_folder.join(format!("{}_auth_store.json", store_key)); - AuthenticationStorage { - store_key: store_key.to_string(), - fallback_storage: fallback_storage::FallbackStorage::new(fallback_location), + /// Create a new authentication storage with no backends + pub fn new() -> Self { + Self { + backends: vec![], cache: Arc::new(Mutex::new(HashMap::new())), } } -} -/// An error that can occur when accessing the authentication storage -#[derive(thiserror::Error, Debug)] -pub enum AuthenticationStorageError { - /// An error occurred when accessing the authentication storage - #[error("Could not retrieve credentials from authentication storage: {0}")] - StorageError(#[from] keyring::Error), - - /// An error occurred when serializing the credentials - #[error("Could not serialize credentials {0}")] - SerializeCredentialsError(#[from] serde_json::Error), - - /// An error occurred when parsing the credentials - #[error("Could not parse credentials stored for {host}")] - ParseCredentialsError { - /// The host for which the credentials could not be parsed - host: String, - }, - - /// An error occurred when accessing the fallback storage - /// (e.g. the JSON file) - #[error("Could not retrieve credentials from fallback storage: {0}")] - FallbackStorageError(#[from] fallback_storage::FallbackStorageError), -} + /// Add a new storage backend to the authentication storage + /// (backends are tried in the order they are added) + pub fn add_backend(&mut self, backend: Arc) { + self.backends.push(backend); + } -impl AuthenticationStorage { /// Store the given authentication information for the given host - pub fn store( - &self, - host: &str, - authentication: &Authentication, - ) -> Result<(), AuthenticationStorageError> { - let entry = Entry::new(&self.store_key, host)?; - let password = serde_json::to_string(authentication)?; - - match entry.set_password(&password) { - Ok(_) => return Ok(()), - Err(e) => { - tracing::warn!( - "Error storing credentials for {}: {}, using fallback storage at {}", - host, - e, - self.fallback_storage.path.display() - ); - self.fallback_storage.set_password(host, &password)?; + pub fn store(&self, host: &str, authentication: &Authentication) -> Result<()> { + { + let mut cache = self.cache.lock().unwrap(); + cache.insert(host.to_string(), Some(authentication.clone())); + } + + for backend in &self.backends { + if let Err(e) = backend.store(host, authentication) { + tracing::warn!("Error storing credentials in backend: {}", e); + } else { + return Ok(()); } } - Ok(()) + + Err(anyhow!("All backends failed to store credentials")) } /// Retrieve the authentication information for the given host - pub fn get(&self, host: &str) -> Result, AuthenticationStorageError> { + pub fn get(&self, host: &str) -> Result> { { let cache = self.cache.lock().unwrap(); if let Some(auth) = cache.get(host) { @@ -96,60 +78,23 @@ impl AuthenticationStorage { } } - let entry = Entry::new(&self.store_key, host)?; - let password = entry.get_password(); - - let p_string = match password { - Ok(password) => password, - Err(keyring::Error::NoEntry) => { - return Ok(None); - } - Err(e) => { - tracing::debug!( - "Unable to retrieve credentials for {}: {}, using fallback credential storage at {}", - host, - e, - self.fallback_storage.path.display() - ); - match self.fallback_storage.get_password(host)? { - None => return Ok(None), - Some(password) => password, + for backend in &self.backends { + match backend.get(host) { + Ok(Some(auth)) => { + let mut cache = self.cache.lock().unwrap(); + cache.insert(host.to_string(), Some(auth.clone())); + return Ok(Some(auth)); + } + Ok(None) => { + return Ok(None); + } + Err(e) => { + tracing::warn!("Error retrieving credentials from backend: {}", e); } - } - }; - - match Authentication::from_str(&p_string) { - Ok(auth) => { - let mut cache = self.cache.lock().unwrap(); - cache.insert(host.to_string(), Some(auth.clone())); - Ok(Some(auth)) - } - Err(err) => { - tracing::warn!("Error parsing credentials for {}: {:?}", host, err); - Err(AuthenticationStorageError::ParseCredentialsError { - host: host.to_string(), - }) - } - } - } - - /// Delete the authentication information for the given host - pub fn delete(&self, host: &str) -> Result<(), AuthenticationStorageError> { - { - let mut cache = self.cache.lock().unwrap(); - cache.remove(host); - } - - let entry = Entry::new(&self.store_key, host)?; - match entry.delete_password() { - Ok(_) => {} - Err(keyring::Error::NoEntry) => {} - Err(e) => { - tracing::warn!("Error deleting credentials for {}: {}", host, e); } } - Ok(self.fallback_storage.delete_password(host)?) + Err(anyhow!("All backends failed to retrieve credentials")) } /// Retrieve the authentication information for the given URL @@ -190,4 +135,28 @@ impl AuthenticationStorage { Ok((url, None)) } } + + /// Delete the authentication information for the given host + pub fn delete(&self, host: &str) -> Result<()> { + { + let mut cache = self.cache.lock().unwrap(); + cache.insert(host.to_string(), None); + } + + let mut all_failed = true; + + for backend in &self.backends { + if let Err(e) = backend.delete(host) { + tracing::warn!("Error deleting credentials from backend: {}", e); + } else { + all_failed = false; + } + } + + if all_failed { + Err(anyhow!("All backends failed to delete credentials")) + } else { + Ok(()) + } + } } diff --git a/crates/rattler_networking/src/lib.rs b/crates/rattler_networking/src/lib.rs index 58b8e522c..69f3829f1 100644 --- a/crates/rattler_networking/src/lib.rs +++ b/crates/rattler_networking/src/lib.rs @@ -43,12 +43,6 @@ pub fn default_auth_store_fallback_directory() -> &'static Path { }) } -impl Default for AuthenticationStorage { - fn default() -> Self { - AuthenticationStorage::new("rattler", default_auth_store_fallback_directory()) - } -} - impl AuthenticatedClient { /// Create a new authenticated client from the given client and authentication storage pub fn from_client(client: Client, auth_storage: AuthenticationStorage) -> AuthenticatedClient { @@ -232,6 +226,10 @@ impl AuthenticatedClientBlocking { #[cfg(test)] mod tests { + use std::sync::Arc; + + use crate::authentication_storage::backends::file::FileStorage; + use super::*; use tempfile::tempdir; @@ -239,7 +237,11 @@ mod tests { #[test] fn test_store_fallback() -> anyhow::Result<()> { let tdir = tempdir()?; - let storage = super::AuthenticationStorage::new("rattler_test", tdir.path()); + let mut storage = AuthenticationStorage::new(); + storage.add_backend(Arc::from(FileStorage::new( + tdir.path().to_path_buf().join("auth.json"), + ))); + let host = "test.example.com"; let authentication = Authentication::CondaToken("testtoken".to_string()); storage.store(host, &authentication)?; @@ -250,7 +252,11 @@ mod tests { #[test] fn test_conda_token_storage() -> anyhow::Result<()> { let tdir = tempdir()?; - let storage = super::AuthenticationStorage::new("rattler_test", tdir.path()); + let mut storage = AuthenticationStorage::new(); + storage.add_backend(Arc::from(FileStorage::new( + tdir.path().to_path_buf().join("auth.json"), + ))); + let host = "conda.example.com"; // Make sure the keyring is empty @@ -292,7 +298,10 @@ mod tests { #[test] fn test_bearer_storage() -> anyhow::Result<()> { let tdir = tempdir()?; - let storage = super::AuthenticationStorage::new("rattler_test", tdir.path()); + let mut storage = AuthenticationStorage::new(); + storage.add_backend(Arc::from(FileStorage::new( + tdir.path().to_path_buf().join("auth.json"), + ))); let host = "bearer.example.com"; // Make sure the keyring is empty @@ -339,7 +348,10 @@ mod tests { #[test] fn test_basic_auth_storage() -> anyhow::Result<()> { let tdir = tempdir()?; - let storage = super::AuthenticationStorage::new("rattler_test", tdir.path()); + let mut storage = AuthenticationStorage::new(); + storage.add_backend(Arc::from(FileStorage::new( + tdir.path().to_path_buf().join("auth.json"), + ))); let host = "basic.example.com"; // Make sure the keyring is empty diff --git a/crates/rattler_repodata_gateway/src/fetch/mod.rs b/crates/rattler_repodata_gateway/src/fetch/mod.rs index dbcd6aa35..caefe0be8 100644 --- a/crates/rattler_repodata_gateway/src/fetch/mod.rs +++ b/crates/rattler_repodata_gateway/src/fetch/mod.rs @@ -1366,7 +1366,6 @@ mod test { pub async fn test_gzip_transfer_encoding() { // Create a directory with some repodata. let subdir_path = TempDir::new().unwrap(); - let tempdir = TempDir::new().unwrap(); write_encoded( FAKE_REPO_DATA.as_ref(), &subdir_path.path().join("repodata.json.gz"), @@ -1383,10 +1382,9 @@ mod test { // Download the data from the channel let cache_dir = TempDir::new().unwrap(); let client = Client::builder().no_gzip().build().unwrap(); - let authenticated_client = AuthenticatedClient::from_client( - client, - AuthenticationStorage::new("rattler", tempdir.path()), - ); + + let authenticated_client = + AuthenticatedClient::from_client(client, AuthenticationStorage::default()); let result = fetch_repo_data( server.url(), authenticated_client,