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

fix: Create parent directories for file storage #1045

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
41 changes: 19 additions & 22 deletions crates/rattler/src/cli/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! This module contains CLI common entrypoint for authentication.
use clap::Parser;
use rattler_networking::{
authentication_storage::backends::file::FileStorageError, Authentication, AuthenticationStorage,
authentication_storage::AuthenticationStorageError, Authentication, AuthenticationStorage,
};
use thiserror;

Expand Down Expand Up @@ -89,20 +89,21 @@ pub enum AuthenticationCLIError {
#[error("Authentication with S3 requires a S3 access key ID and a secret access key. Use `--s3-access-key-id` and `--s3-secret-access-key` to provide them")]
S3BadMethod,

// TODO: rework this
/// Wrapper for errors that are generated from the underlying storage system
/// (keyring or file system)
#[error("Failed to initialize the authentication storage system")]
InitializeStorageError(#[source] FileStorageError),
#[error("Failed to interact with the authentication storage system")]
AnyhowError(#[from] anyhow::Error),

/// Wrapper for errors that are generated from the underlying storage system
/// (keyring or file system)
#[error("Failed to interact with the authentication storage system")]
StorageError(#[source] anyhow::Error),
AuthenticationStorageError(#[from] AuthenticationStorageError),
}

fn get_url(url: &str) -> Result<String, AuthenticationCLIError> {
// parse as url and extract host without scheme or port
let host = if url.contains("http://") || url.contains("https://") {
let host = if url.contains("://") {
url::Url::parse(url)?.host_str().unwrap().to_string()
} else {
url.to_string()
Expand All @@ -119,9 +120,6 @@ fn get_url(url: &str) -> Result<String, AuthenticationCLIError> {
}

fn login(args: LoginArgs, storage: AuthenticationStorage) -> Result<(), AuthenticationCLIError> {
let host = get_url(&args.host)?;
println!("Authenticating with {host}");

let auth = if let Some(conda_token) = args.conda_token {
Authentication::CondaToken(conda_token)
} else if let Some(username) = args.username {
Expand All @@ -146,25 +144,27 @@ fn login(args: LoginArgs, storage: AuthenticationStorage) -> Result<(), Authenti
return Err(AuthenticationCLIError::NoAuthenticationMethod);
};

if host.contains("prefix.dev") && !matches!(auth, Authentication::BearerToken(_)) {
if args.host.contains("prefix.dev") && !matches!(auth, Authentication::BearerToken(_)) {
return Err(AuthenticationCLIError::PrefixDevBadMethod);
}

if host.contains("anaconda.org") && !matches!(auth, Authentication::CondaToken(_)) {
if args.host.contains("anaconda.org") && !matches!(auth, Authentication::CondaToken(_)) {
return Err(AuthenticationCLIError::AnacondaOrgBadMethod);
}

if host.starts_with("s3://") && !matches!(auth, Authentication::S3Credentials { .. }) {
if args.host.contains("s3://") && !matches!(auth, Authentication::S3Credentials { .. })
|| matches!(auth, Authentication::S3Credentials { .. }) && !args.host.contains("s3://")
{
return Err(AuthenticationCLIError::S3BadMethod);
}

if matches!(auth, Authentication::S3Credentials { .. }) && !host.starts_with("s3://") {
return Err(AuthenticationCLIError::S3BadMethod);
}
let host = get_url(&args.host)?;
eprintln!(
"Authenticating with {host} using {} methode",
auth.methode()
);

storage
.store(&host, &auth)
.map_err(AuthenticationCLIError::StorageError)?;
storage.store(&host, &auth)?;
Ok(())
}

Expand All @@ -173,16 +173,13 @@ fn logout(args: LogoutArgs, storage: AuthenticationStorage) -> Result<(), Authen

println!("Removing authentication for {host}");

storage
.delete(&host)
.map_err(AuthenticationCLIError::StorageError)?;
storage.delete(&host)?;
Ok(())
}

/// CLI entrypoint for authentication
pub async fn execute(args: Args) -> Result<(), AuthenticationCLIError> {
let storage = AuthenticationStorage::from_env_and_defaults()
.map_err(AuthenticationCLIError::InitializeStorageError)?;
let storage = AuthenticationStorage::from_env_and_defaults()?;

match args.subcommand {
Subcommand::Login(args) => login(args, storage),
Expand Down
4 changes: 2 additions & 2 deletions crates/rattler_networking/src/authentication_middleware.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! `reqwest` middleware that authenticates requests with data from the `AuthenticationStorage`
use crate::authentication_storage::backends::file::FileStorageError;
use crate::authentication_storage::AuthenticationStorageError;
use crate::{Authentication, AuthenticationStorage};
use async_trait::async_trait;
use base64::prelude::BASE64_STANDARD;
Expand Down Expand Up @@ -55,7 +55,7 @@ impl AuthenticationMiddleware {
}

/// Create a new authentication middleware with the default authentication storage
pub fn from_env_and_defaults() -> Result<Self, FileStorageError> {
pub fn from_env_and_defaults() -> Result<Self, AuthenticationStorageError> {
Ok(Self {
auth_storage: AuthenticationStorage::from_env_and_defaults()?,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ impl FromStr for Authentication {
serde_json::from_str(s).map_err(|_err| AuthenticationParseError::InvalidToken)
}
}

impl Authentication {
/// Get the scheme of the authentication method
pub fn methode(&self) -> &str {
match self {
Authentication::BearerToken(_) => "BearerToken",
Authentication::BasicHTTP { .. } => "BasicHTTP",
Authentication::CondaToken(_) => "CondaToken",
Authentication::S3Credentials { .. } => "S3",
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! file storage for passwords.
use anyhow::Result;
use async_fd_lock::{
blocking::{LockRead, LockWrite},
RwLockWriteGuard,
Expand All @@ -10,7 +9,7 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};

use crate::authentication_storage::StorageBackend;
use crate::authentication_storage::{AuthenticationStorageError, StorageBackend};
use crate::Authentication;

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -99,6 +98,14 @@ impl FileStorage {

/// Serialize the given `BTreeMap` and write it to the JSON file
fn write_json(&self, dict: &BTreeMap<String, Authentication>) -> Result<(), FileStorageError> {
let parent = self
.path
.parent()
.ok_or(FileStorageError::IOError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Parent directory not found",
)))?;
std::fs::create_dir_all(parent)?;
Comment on lines +101 to +108
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the actual issue

let write_guard: std::result::Result<
RwLockWriteGuard<File>,
async_fd_lock::LockError<File>,
Expand All @@ -121,18 +128,22 @@ impl FileStorage {
}

impl StorageBackend for FileStorage {
fn store(&self, host: &str, authentication: &crate::Authentication) -> Result<()> {
fn store(
&self,
host: &str,
authentication: &crate::Authentication,
) -> Result<(), AuthenticationStorageError> {
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>> {
fn get(&self, host: &str) -> Result<Option<crate::Authentication>, AuthenticationStorageError> {
let cache = self.cache.read().unwrap();
Ok(cache.content.get(host).cloned())
}

fn delete(&self, host: &str) -> Result<()> {
fn delete(&self, host: &str) -> Result<(), AuthenticationStorageError> {
let mut dict = self.read_json()?;
if dict.remove(host).is_some() {
Ok(self.write_json(&dict)?)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! 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};
use crate::{
authentication_storage::{AuthenticationStorageError, StorageBackend},
Authentication,
};

#[derive(Clone, Debug)]
/// A storage backend that stores credentials in the operating system's keyring
Expand All @@ -26,6 +28,7 @@ impl KeyringAuthenticationStorage {
/// An error that can occur when accessing the authentication storage
#[derive(thiserror::Error, Debug)]
pub enum KeyringAuthenticationStorageError {
// TODO: make this more fine-grained
/// An error occurred when accessing the authentication storage
#[error("Could not retrieve credentials from authentication storage: {0}")]
StorageError(#[from] keyring::Error),
Expand All @@ -49,23 +52,32 @@ impl Default for KeyringAuthenticationStorage {
}

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)?;
fn store(
&self,
host: &str,
authentication: &Authentication,
) -> Result<(), AuthenticationStorageError> {
let password = serde_json::to_string(authentication)
.map_err(KeyringAuthenticationStorageError::from)?;
let entry =
Entry::new(&self.store_key, host).map_err(KeyringAuthenticationStorageError::from)?;

entry
.set_password(&password)
.map_err(KeyringAuthenticationStorageError::from)?;

Ok(())
}

fn get(&self, host: &str) -> Result<Option<Authentication>> {
let entry = Entry::new(&self.store_key, host)?;
fn get(&self, host: &str) -> Result<Option<Authentication>, AuthenticationStorageError> {
let entry =
Entry::new(&self.store_key, host).map_err(KeyringAuthenticationStorageError::from)?;
let password = entry.get_password();

let p_string = match password {
Ok(password) => password,
Err(keyring::Error::NoEntry) => return Ok(None),
Err(e) => return Err(e.into()),
Err(e) => return Err(KeyringAuthenticationStorageError::from(e))?,
};

match Authentication::from_str(&p_string) {
Expand All @@ -80,9 +92,12 @@ impl StorageBackend for KeyringAuthenticationStorage {
}
}

fn delete(&self, host: &str) -> Result<()> {
let entry = Entry::new(&self.store_key, host)?;
entry.delete_credential()?;
fn delete(&self, host: &str) -> Result<(), AuthenticationStorageError> {
let entry =
Entry::new(&self.store_key, host).map_err(KeyringAuthenticationStorageError::from)?;
entry
.delete_credential()
.map_err(KeyringAuthenticationStorageError::from)?;

Ok(())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Read authentication credentials from `.netrc` files.

use crate::{authentication_storage::StorageBackend, Authentication};
use crate::{
authentication_storage::{AuthenticationStorageError, StorageBackend},
Authentication,
};
use netrc_rs::{Machine, Netrc};
use std::{collections::HashMap, env, io::ErrorKind, path::Path, path::PathBuf};

Expand All @@ -22,6 +25,10 @@ pub enum NetRcStorageError {
/// An error occurred when parsing the netrc file
#[error("could not parse .netc file: {0}")]
ParseError(netrc_rs::Error),

/// Something is not supported
#[error("{0}")]
NotSupportedError(String),
}

impl NetRcStorage {
Expand Down Expand Up @@ -85,19 +92,27 @@ impl NetRcStorage {
}

impl StorageBackend for NetRcStorage {
fn store(&self, _host: &str, _authentication: &Authentication) -> anyhow::Result<()> {
anyhow::bail!("NetRcStorage does not support storing credentials")
fn store(
&self,
_host: &str,
_authentication: &Authentication,
) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support storing credentials".to_string(),
))?
}

fn delete(&self, _host: &str) -> anyhow::Result<()> {
anyhow::bail!("NetRcStorage does not support deleting credentials")
fn delete(&self, _host: &str) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support deleting credentials".to_string(),
))?
}

fn get(&self, host: &str) -> anyhow::Result<Option<Authentication>> {
fn get(&self, host: &str) -> Result<Option<Authentication>, AuthenticationStorageError> {
match self.get_password(host) {
Ok(Some(auth)) => Ok(Some(auth)),
Ok(None) => Ok(None),
Err(err) => Err(anyhow::Error::new(err)),
Err(err) => Err(err.into()),
}
}
}
Expand Down
27 changes: 23 additions & 4 deletions crates/rattler_networking/src/authentication_storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
//! This module contains the authentication storage backend trait and implementations
use self::authentication::Authentication;
use anyhow::Result;

pub mod authentication;
pub mod backends;
pub mod storage;

/// An error occurred when accessing the authentication storage
#[derive(thiserror::Error, Debug)]
pub enum AuthenticationStorageError {
/// An error occurred when accessing the file storage
#[error("FileStorageError")]
FileStorageError(#[from] crate::authentication_storage::backends::file::FileStorageError),
/// An error occurred when accessing the keyring storage
#[error("KeyringStorageError")]
KeyringStorageError(
#[from] crate::authentication_storage::backends::keyring::KeyringAuthenticationStorageError,
),
/// An error occurred when accessing the netrc storage
#[error("NetRcStorageError")]
NetRcStorageError(#[from] crate::authentication_storage::backends::netrc::NetRcStorageError),
}

/// 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<()>;
fn store(
&self,
host: &str,
authentication: &Authentication,
) -> Result<(), AuthenticationStorageError>;

/// Retrieve the authentication information for the given host
fn get(&self, host: &str) -> Result<Option<Authentication>>;
fn get(&self, host: &str) -> Result<Option<Authentication>, AuthenticationStorageError>;

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