diff --git a/src/lib.rs b/src/lib.rs index 7ffc2afaf..25f8f1aaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub mod system_tools; pub mod tool_configuration; #[cfg(feature = "tui")] pub mod tui; +mod url_with_trailing_slash; pub mod used_variables; pub mod utils; pub mod variant_config; @@ -69,6 +70,7 @@ use recipe::parser::{find_outputs_from_src, Dependency, TestType}; use selectors::SelectorConfig; use system_tools::SystemTools; use tool_configuration::{Configuration, TestStrategy}; +use tracing::warn; use variant_config::VariantConfig; use crate::metadata::PlatformWithVirtualPackages; @@ -720,54 +722,73 @@ pub async fn upload_from_args(args: UploadOpts) -> miette::Result<()> { &store, quetz_opts.api_key, &args.package_files, - quetz_opts.url, + quetz_opts.url.into(), quetz_opts.channel, ) - .await?; + .await } ServerType::Artifactory(artifactory_opts) => { - upload::upload_package_to_artifactory( - &store, + let token = match ( artifactory_opts.username, artifactory_opts.password, + artifactory_opts.token, + ) { + (_, _, Some(token)) => Some(token), + (Some(_), Some(password), _) => { + warn!("Using username and password for Artifactory authentication is deprecated, using password as token. Please use an API token instead."); + Some(password) + } + (Some(_), None, _) => { + return Err(miette::miette!( + "Artifactory username provided without a password" + )); + } + (None, Some(_), _) => { + return Err(miette::miette!( + "Artifactory password provided without a username" + )); + } + _ => None, + }; + upload::upload_package_to_artifactory( + &store, + token, &args.package_files, - artifactory_opts.url, + artifactory_opts.url.into(), artifactory_opts.channel, ) - .await?; + .await } ServerType::Prefix(prefix_opts) => { upload::upload_package_to_prefix( &store, prefix_opts.api_key, &args.package_files, - prefix_opts.url, + prefix_opts.url.into(), prefix_opts.channel, ) - .await?; + .await } ServerType::Anaconda(anaconda_opts) => { upload::upload_package_to_anaconda( &store, anaconda_opts.api_key, &args.package_files, - anaconda_opts.url, + anaconda_opts.url.into(), anaconda_opts.owner, anaconda_opts.channel, anaconda_opts.force, ) - .await?; + .await } ServerType::CondaForge(conda_forge_opts) => { upload::conda_forge::upload_packages_to_conda_forge( conda_forge_opts, &args.package_files, ) - .await?; + .await } } - - Ok(()) } /// Sort the build outputs (recipes) topologically based on their dependencies. diff --git a/src/opt.rs b/src/opt.rs index 9cd66e285..6b5ce2427 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -537,12 +537,16 @@ pub struct ArtifactoryOpts { pub channel: String, /// Your Artifactory username - #[arg(short = 'r', long, env = "ARTIFACTORY_USERNAME")] + #[arg(long, env = "ARTIFACTORY_USERNAME", hide = true)] pub username: Option, /// Your Artifactory password - #[arg(short, long, env = "ARTIFACTORY_PASSWORD")] + #[arg(long, env = "ARTIFACTORY_PASSWORD", hide = true)] pub password: Option, + + /// Your Artifactory token + #[arg(short, long, env = "ARTIFACTORY_TOKEN")] + pub token: Option, } /// Options for uploading to a prefix.dev server. diff --git a/src/upload/anaconda.rs b/src/upload/anaconda.rs index dc3b772d5..4a04f9be8 100644 --- a/src/upload/anaconda.rs +++ b/src/upload/anaconda.rs @@ -12,12 +12,14 @@ use tracing::debug; use tracing::info; use url::Url; +use crate::url_with_trailing_slash::UrlWithTrailingSlash; + use super::package::ExtractedPackage; use super::VERSION; pub struct Anaconda { client: Client, - url: Url, + url: UrlWithTrailingSlash, } #[derive(Serialize, Deserialize, Debug)] @@ -45,7 +47,7 @@ struct FileStageResponse { } impl Anaconda { - pub fn new(token: String, url: Url) -> Self { + pub fn new(token: String, url: UrlWithTrailingSlash) -> Self { let mut default_headers = reqwest::header::HeaderMap::new(); default_headers.append( diff --git a/src/upload/conda_forge.rs b/src/upload/conda_forge.rs index 68a97d1d3..d672b60df 100644 --- a/src/upload/conda_forge.rs +++ b/src/upload/conda_forge.rs @@ -48,7 +48,7 @@ pub async fn upload_packages_to_conda_forge( opts: CondaForgeOpts, package_files: &Vec, ) -> miette::Result<()> { - let anaconda = anaconda::Anaconda::new(opts.staging_token, opts.anaconda_url); + let anaconda = anaconda::Anaconda::new(opts.staging_token, opts.anaconda_url.into()); let mut channels: HashMap> = HashMap::new(); diff --git a/src/upload/mod.rs b/src/upload/mod.rs index f75a1b841..291178deb 100644 --- a/src/upload/mod.rs +++ b/src/upload/mod.rs @@ -10,11 +10,12 @@ use std::{ use tokio_util::io::ReaderStream; use trusted_publishing::{check_trusted_publishing, TrustedPublishResult}; +use crate::url_with_trailing_slash::UrlWithTrailingSlash; use miette::{Context, IntoDiagnostic}; use rattler_networking::{Authentication, AuthenticationStorage}; use rattler_redaction::Redact; use reqwest::Method; -use tracing::info; +use tracing::{info, warn}; use url::Url; use crate::upload::package::{sha256_sum, ExtractedPackage}; @@ -57,12 +58,12 @@ pub async fn upload_package_to_quetz( storage: &AuthenticationStorage, api_key: Option, package_files: &Vec, - url: Url, + url: UrlWithTrailingSlash, channel: String, ) -> miette::Result<()> { let token = match api_key { Some(api_key) => api_key, - None => match storage.get_by_url(url.clone()) { + None => match storage.get_by_url(Url::from(url.clone())) { Ok((_, Some(Authentication::CondaToken(token)))) => token, Ok((_, Some(_))) => { return Err(miette::miette!("A Conda token is required for authentication with quetz. @@ -110,27 +111,33 @@ pub async fn upload_package_to_quetz( /// Uploads package files to an Artifactory server. pub async fn upload_package_to_artifactory( storage: &AuthenticationStorage, - username: Option, - password: Option, + token: Option, package_files: &Vec, - url: Url, + url: UrlWithTrailingSlash, channel: String, ) -> miette::Result<()> { - let (username, password) = match (username, password) { - (Some(u), Some(p)) => (u, p), - (Some(_), _) | (_, Some(_)) => { - return Err(miette::miette!("A username and password is required for authentication with artifactory, only one was given")); - } - _ => match storage.get_by_url(url.clone()) { - Ok((_, Some(Authentication::BasicHTTP { username, password }))) => (username, password), + let token = match token { + Some(t) => t, + _ => match storage.get_by_url(Url::from(url.clone())) { + Ok((_, Some(Authentication::BearerToken(token)))) => token, + Ok(( + _, + Some(Authentication::BasicHTTP { + username: _, + password, + }), + )) => { + warn!("A bearer token is required for authentication with artifactory. Using the password from the keychain / auth file to authenticate. Consider switching to a bearer token instead for Artifactory."); + password + } Ok((_, Some(_))) => { - return Err(miette::miette!("A username and password is required for authentication with artifactory. - Authentication information found in the keychain / auth file, but it was not a username and password")); + return Err(miette::miette!("A bearer token is required for authentication with artifactory. + Authentication information found in the keychain / auth file, but it was not a bearer token")); } Ok((_, None)) => { return Err(miette::miette!( - "No username and password was given and none was found in the keychain / auth file" - )); + "No bearer token was given and none was found in the keychain / auth file" + )); } Err(e) => { return Err(miette::miette!( @@ -163,7 +170,7 @@ pub async fn upload_package_to_artifactory( let prepared_request = client .request(Method::PUT, upload_url) - .basic_auth(username.clone(), Some(password.clone())); + .bearer_auth(token.clone()); send_request(prepared_request, package_file).await?; } @@ -178,11 +185,11 @@ pub async fn upload_package_to_prefix( storage: &AuthenticationStorage, api_key: Option, package_files: &Vec, - url: Url, + url: UrlWithTrailingSlash, channel: String, ) -> miette::Result<()> { let check_storage = || { - match storage.get_by_url(url.clone()) { + match storage.get_by_url(Url::from(url.clone())) { Ok((_, Some(Authentication::BearerToken(token)))) => Ok(token), Ok((_, Some(_))) => { Err(miette::miette!("A Conda token is required for authentication with prefix.dev. @@ -251,7 +258,7 @@ pub async fn upload_package_to_anaconda( storage: &AuthenticationStorage, token: Option, package_files: &Vec, - url: Url, + url: UrlWithTrailingSlash, owner: String, channels: Vec, force: bool, diff --git a/src/url_with_trailing_slash.rs b/src/url_with_trailing_slash.rs new file mode 100644 index 000000000..7cd677349 --- /dev/null +++ b/src/url_with_trailing_slash.rs @@ -0,0 +1,82 @@ +// copied from rattler-conda-types::utils::url_with_trailing_slash +// TODO: move to separate crate + +use std::{ + fmt::{Display, Formatter}, + ops::Deref, + str::FromStr, +}; + +use rattler_redaction::Redact; +use serde::{Deserialize, Deserializer, Serialize}; +use url::Url; + +/// A URL that always has a trailing slash. A trailing slash in a URL has +/// significance but users often forget to add it. This type is used to +/// normalize the use of the URL. +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize)] +#[serde(transparent)] +pub struct UrlWithTrailingSlash(Url); + +impl Deref for UrlWithTrailingSlash { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for UrlWithTrailingSlash { + fn as_ref(&self) -> &Url { + &self.0 + } +} + +impl From for UrlWithTrailingSlash { + fn from(url: Url) -> Self { + let path = url.path(); + if path.ends_with('/') { + Self(url) + } else { + let mut url = url.clone(); + url.set_path(&format!("{path}/")); + Self(url) + } + } +} + +impl<'de> Deserialize<'de> for UrlWithTrailingSlash { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let url = Url::deserialize(deserializer)?; + Ok(url.into()) + } +} + +impl FromStr for UrlWithTrailingSlash { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + Ok(Url::parse(s)?.into()) + } +} + +impl From for Url { + fn from(value: UrlWithTrailingSlash) -> Self { + value.0 + } +} + +impl Display for UrlWithTrailingSlash { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} + +impl Redact for UrlWithTrailingSlash { + fn redact(self) -> Self { + UrlWithTrailingSlash(self.0.redact()) + } +}