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: Use UrlWithTrailingSlash for upload, use bearer auth for Artifactory upload #1280

Merged
merged 1 commit into from
Dec 23, 2024
Merged
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
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())
}
}
Loading