Skip to content

Commit

Permalink
feat: generalised CLI authentication (#537)
Browse files Browse the repository at this point in the history
Co-authored-by: rph <[email protected]>
  • Loading branch information
kassoulait and rph authored Feb 27, 2024
1 parent b08af41 commit 82dc20f
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 6 deletions.
6 changes: 0 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ resolver = "2"
[profile.dev.package.insta]
opt-level = 3

[profile.dev.package.similar]
opt-level = 3

[workspace.package]
version = "0.19.0"
categories = ["conda"]
Expand All @@ -31,11 +28,9 @@ anyhow = "1.0.79"
assert_matches = "1.5.0"
async-compression = { version = "0.4.6", features = ["gzip", "tokio", "bzip2", "zstd"] }
async-trait = "0.1.77"
async_zip = { version = "0.0.16", default-features = false }
axum = { version = "0.7.4", default-features = false, features = ["tokio", "http1"] }
base64 = "0.21.7"
bindgen = "0.69.4"
bisection = "0.1.0"
blake2 = "0.10.6"
bytes = "1.5.0"
bzip2 = "0.4.4"
Expand All @@ -61,7 +56,6 @@ getrandom = { version = "0.2.12", default-features = false }
glob = "0.3.1"
hex = "0.4.3"
hex-literal = "0.4.1"
http-content-range = "0.1.2"
humansize = "2.1.3"
humantime = "2.1.0"
indexmap = "2.2.2"
Expand Down
2 changes: 2 additions & 0 deletions crates/rattler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ readme.workspace = true
default = ['native-tls']
native-tls = ['reqwest/native-tls', 'rattler_package_streaming/native-tls']
rustls-tls = ['reqwest/rustls-tls', 'rattler_package_streaming/rustls-tls']
cli-tools = ['dep:clap']

[dependencies]
anyhow = { workspace = true }
async-compression = { workspace = true }
bytes = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, optional = true }
digest = { workspace = true }
dirs = { workspace = true }
drop_bomb = { workspace = true }
Expand Down
150 changes: 150 additions & 0 deletions crates/rattler/src/cli/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! This module contains CLI common entrypoint for authentication.
use clap::Parser;
use rattler_networking::{Authentication, AuthenticationStorage};
use thiserror;

/// Command line arguments that contain authentication data
#[derive(Parser, Debug)]
pub struct LoginArgs {
/// The host to authenticate with (e.g. repo.prefix.dev)
host: String,

/// The token to use (for authentication with prefix.dev)
#[clap(long)]
token: Option<String>,

/// The username to use (for basic HTTP authentication)
#[clap(long)]
username: Option<String>,

/// The password to use (for basic HTTP authentication)
#[clap(long)]
password: Option<String>,

/// The token to use on anaconda.org / quetz authentication
#[clap(long)]
conda_token: Option<String>,
}

#[derive(Parser, Debug)]
struct LogoutArgs {
/// The host to remove authentication for
host: String,
}

#[derive(Parser, Debug)]
enum Subcommand {
/// Store authentication information for a given host
Login(LoginArgs),
/// Remove authentication information for a given host
Logout(LogoutArgs),
}

/// Login to prefix.dev or anaconda.org servers to access private channels
#[derive(Parser, Debug)]
pub struct Args {
#[clap(subcommand)]
subcommand: Subcommand,
}

/// Authentication errors that can be returned by the AuthenticationCLIError
#[derive(thiserror::Error, Debug)]
pub enum AuthenticationCLIError {
/// An error occured when the input repository URL is parsed
#[error("Failed to parse the URL")]
ParseUrlError(#[from] url::ParseError),

/// Basic authentication needs a username and a password. The password is
/// missing here.
#[error("Password must be provided when using basic authentication.")]
MissingPassword,

/// Authentication has not been provided in the input parameters.
#[error("No authentication method provided.")]
NoAuthenticationMethod,

/// Bad authentication method when using prefix.dev
#[error("Authentication with prefix.dev requires a token. Use `--token` to provide one.")]
PrefixDevBadMethod,

/// Bad authentication method when using anaconda.org
#[error("Authentication with anaconda.org requires a conda token. Use `--conda-token` to provide one.")]
AnacondaOrgBadMethod,

/// 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),
}

fn get_url(url: &str) -> Result<String, AuthenticationCLIError> {
// parse as url and extract host without scheme or port
let host = if url.contains("://") {
url::Url::parse(url)?.host_str().unwrap().to_string()
} else {
url.to_string()
};

let host = if host.matches('.').count() == 1 {
// use wildcard for top-level domains
format!("*.{}", host)
} else {
host
};

Ok(host)
}

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 {
if args.password.is_none() {
return Err(AuthenticationCLIError::MissingPassword);
} else {
let password = args.password.unwrap();
Authentication::BasicHTTP { username, password }
}
} else if let Some(token) = args.token {
Authentication::BearerToken(token)
} else {
return Err(AuthenticationCLIError::NoAuthenticationMethod);
};

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

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

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

fn logout(args: LogoutArgs, storage: AuthenticationStorage) -> Result<(), AuthenticationCLIError> {
let host = get_url(&args.host)?;

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

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

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

match args.subcommand {
Subcommand::Login(args) => login(args, storage),
Subcommand::Logout(args) => logout(args, storage),
}
}
3 changes: 3 additions & 0 deletions crates/rattler/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! This module contains CLI common components used in various sub-projects
//! (like pixi, rattler-build).
pub mod auth;
2 changes: 2 additions & 0 deletions crates/rattler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

use std::path::PathBuf;

#[cfg(feature = "cli-tools")]
pub mod cli;
pub mod install;
pub mod package_cache;
pub mod validation;
Expand Down

0 comments on commit 82dc20f

Please sign in to comment.