Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add option to force usage of fallback_auth_store #435

Merged
merged 9 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions crates/rattler-bin/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,94 +1,75 @@
//! 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
#[error("JSON error: {0}")]
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<LockFile, FallbackStorageError> {
fn lock(&self) -> Result<LockFile, FileStorageError> {
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<Option<String>, 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<HashMap<String, String>, FallbackStorageError> {
fn read_json(&self) -> Result<HashMap<String, Authentication>, FileStorageError> {
if !self.path.exists() {
static WARN_GUARD: Lazy<Mutex<HashSet<PathBuf>>> =
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()
);
}
Expand All @@ -101,40 +82,73 @@ impl FallbackStorage {
}

/// Serialize the given HashMap and write it to the JSON file
fn write_json(&self, dict: &HashMap<String, String>) -> Result<(), FallbackStorageError> {
fn write_json(&self, dict: &HashMap<String, Authentication>) -> Result<(), FileStorageError> {
let file = std::fs::File::create(&self.path)?;
let writer = std::io::BufWriter::new(file);
serde_json::to_writer(writer, dict)?;
Ok(())
}
}

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<Option<crate::Authentication>> {
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::*;
use std::io::Write;
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());
}
}
Original file line number Diff line number Diff line change
@@ -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<Option<Authentication>> {
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(())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Multiple backends for storing authentication data.

pub mod file;
pub mod keyring;
19 changes: 17 additions & 2 deletions crates/rattler_networking/src/authentication_storage/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Option<Authentication>>;

/// Delete the authentication information for the given host
fn delete(&self, host: &str) -> Result<()>;
}
Loading