Skip to content

Commit

Permalink
fix: Use UrlWithTrailingSlash for upload, use bearer auth for Artifac…
Browse files Browse the repository at this point in the history
…tory upload
  • Loading branch information
pavelzw committed Dec 19, 2024
1 parent 69b1d95 commit d3b96b6
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 39 deletions.
47 changes: 34 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,6 +71,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;
Expand Down Expand Up @@ -721,54 +723,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.
Expand Down
8 changes: 6 additions & 2 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Your Artifactory password
#[arg(short, long, env = "ARTIFACTORY_PASSWORD")]
#[arg(long, env = "ARTIFACTORY_PASSWORD", hide = true)]
pub password: Option<String>,

/// Your Artifactory token
#[arg(short, long, env = "ARTIFACTORY_TOKEN")]
pub token: Option<String>,
}

/// Options for uploading to a prefix.dev server.
Expand Down
6 changes: 4 additions & 2 deletions src/upload/anaconda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/upload/conda_forge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub async fn upload_packages_to_conda_forge(
opts: CondaForgeOpts,
package_files: &Vec<PathBuf>,
) -> 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<String, HashMap<_, _>> = HashMap::new();

Expand Down
49 changes: 28 additions & 21 deletions src/upload/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -57,12 +58,12 @@ pub async fn upload_package_to_quetz(
storage: &AuthenticationStorage,
api_key: Option<String>,
package_files: &Vec<PathBuf>,
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.
Expand Down Expand Up @@ -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<String>,
password: Option<String>,
token: Option<String>,
package_files: &Vec<PathBuf>,
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!(
Expand Down Expand Up @@ -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?;
}
Expand All @@ -178,11 +185,11 @@ pub async fn upload_package_to_prefix(
storage: &AuthenticationStorage,
api_key: Option<String>,
package_files: &Vec<PathBuf>,
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.
Expand Down Expand Up @@ -251,7 +258,7 @@ pub async fn upload_package_to_anaconda(
storage: &AuthenticationStorage,
token: Option<String>,
package_files: &Vec<PathBuf>,
url: Url,
url: UrlWithTrailingSlash,
owner: String,
channels: Vec<String>,
force: bool,
Expand Down
82 changes: 82 additions & 0 deletions src/url_with_trailing_slash.rs
Original file line number Diff line number Diff line change
@@ -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<Url> for UrlWithTrailingSlash {
fn as_ref(&self) -> &Url {
&self.0
}
}

impl From<Url> 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<D>(deserializer: D) -> Result<Self, D::Error>
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<Self, Self::Err> {
Ok(Url::parse(s)?.into())
}
}

impl From<UrlWithTrailingSlash> 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())
}
}

0 comments on commit d3b96b6

Please sign in to comment.