diff --git a/Cargo.toml b/Cargo.toml index b25a4fb8e..2d9f7ab81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] @@ -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" @@ -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" diff --git a/crates/rattler/Cargo.toml b/crates/rattler/Cargo.toml index 0b6a8abb0..52d6bd864 100644 --- a/crates/rattler/Cargo.toml +++ b/crates/rattler/Cargo.toml @@ -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 } diff --git a/crates/rattler/src/cli/auth.rs b/crates/rattler/src/cli/auth.rs new file mode 100644 index 000000000..efc357fe9 --- /dev/null +++ b/crates/rattler/src/cli/auth.rs @@ -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, + + /// The username to use (for basic HTTP authentication) + #[clap(long)] + username: Option, + + /// The password to use (for basic HTTP authentication) + #[clap(long)] + password: Option, + + /// The token to use on anaconda.org / quetz authentication + #[clap(long)] + conda_token: Option, +} + +#[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 { + // 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), + } +} diff --git a/crates/rattler/src/cli/mod.rs b/crates/rattler/src/cli/mod.rs new file mode 100644 index 000000000..9f2fb8cc1 --- /dev/null +++ b/crates/rattler/src/cli/mod.rs @@ -0,0 +1,3 @@ +//! This module contains CLI common components used in various sub-projects +//! (like pixi, rattler-build). +pub mod auth; diff --git a/crates/rattler/src/lib.rs b/crates/rattler/src/lib.rs index 9d32f312e..64b673785 100644 --- a/crates/rattler/src/lib.rs +++ b/crates/rattler/src/lib.rs @@ -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;