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: redact secrets in the canonical_name functions #801

Merged
merged 12 commits into from
Aug 2, 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
1 change: 1 addition & 0 deletions crates/rattler_conda_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ tracing = { workspace = true }
typed-path = { workspace = true }
url = { workspace = true, features = ["serde"] }
indexmap = { workspace = true }
rattler_redaction = { version = "0.1.0", path = "../rattler_redaction" }

[dev-dependencies]
rand = { workspace = true }
Expand Down
67 changes: 58 additions & 9 deletions crates/rattler_conda_types/src/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use thiserror::Error;
use typed_path::{Utf8NativePathBuf, Utf8TypedPath, Utf8TypedPathBuf};
use url::Url;

use rattler_redaction::Redact;

use super::{ParsePlatformError, Platform};
use crate::utils::{path::is_path, url::parse_scheme};

Expand Down Expand Up @@ -51,12 +53,23 @@ impl ChannelConfig {
}
}

/// Strip the channel alias if the base url is "under" the channel alias.
/// This returns the name of the channel (for example "conda-forge" for
/// `https://conda.anaconda.org/conda-forge` when the channel alias is
/// `https://conda.anaconda.org`).
pub fn strip_channel_alias(&self, base_url: &Url) -> Option<String> {
base_url
.as_str()
.strip_prefix(self.channel_alias.as_str())
.map(|s| s.trim_end_matches('/').to_string())
}

/// Returns the canonical name of a channel with the given base url.
pub fn canonical_name(&self, base_url: &Url) -> NamedChannelOrUrl {
pub fn canonical_name(&self, base_url: &Url) -> String {
if let Some(stripped) = base_url.as_str().strip_prefix(self.channel_alias.as_str()) {
NamedChannelOrUrl::Name(stripped.trim_matches('/').to_string())
stripped.trim_end_matches('/').to_string()
} else {
NamedChannelOrUrl::Url(base_url.clone())
base_url.clone().redact().to_string()
}
}
}
Expand Down Expand Up @@ -102,10 +115,7 @@ impl NamedChannelOrUrl {
pub fn into_channel(self, config: &ChannelConfig) -> Channel {
let name = match &self {
NamedChannelOrUrl::Name(name) => Some(name.clone()),
NamedChannelOrUrl::Url(base_url) => match config.canonical_name(base_url) {
NamedChannelOrUrl::Name(name) => Some(name),
NamedChannelOrUrl::Url(_) => None,
},
NamedChannelOrUrl::Url(base_url) => config.strip_channel_alias(base_url),
};
let base_url = self.into_base_url(config);
Channel {
Expand Down Expand Up @@ -348,7 +358,7 @@ impl Channel {

/// Returns the canonical name of the channel
pub fn canonical_name(&self) -> String {
self.base_url.to_string()
self.base_url.clone().redact().to_string()
}
}

Expand Down Expand Up @@ -648,6 +658,28 @@ mod tests {
assert!(!is_path("conda-forge/label/rust_dev"));
}

#[test]
fn channel_canonical_name() {
let config = ChannelConfig::default_with_root_dir(std::env::current_dir().unwrap());
let channel = Channel::from_str("http://localhost:1234", &config).unwrap();

assert_eq!(channel.canonical_name(), "http://localhost:1234/");

let channel = Channel::from_str("http://user:password@localhost:1234", &config).unwrap();

assert_eq!(
channel.canonical_name(),
"http://user:********@localhost:1234/"
);

let channel =
Channel::from_str("http://localhost:1234/t/secretfoo/blablub", &config).unwrap();
assert_eq!(
channel.canonical_name(),
"http://localhost:1234/t/********/blablub/"
);
}

#[test]
fn config_canonical_name() {
let channel_config = ChannelConfig {
Expand All @@ -664,7 +696,24 @@ mod tests {
channel_config
.canonical_name(&Url::from_str("https://prefix.dev/conda-forge/").unwrap())
.as_str(),
"https://prefix.dev/conda-forge"
"https://prefix.dev/conda-forge/"
);
assert_eq!(
channel_config
.canonical_name(
&Url::from_str("https://prefix.dev/t/mysecrettoken/conda-forge/").unwrap()
)
.as_str(),
"https://prefix.dev/t/********/conda-forge/"
);

assert_eq!(
channel_config
.canonical_name(
&Url::from_str("https://user:secret@prefix.dev/conda-forge/").unwrap()
)
.as_str(),
"https://user:********@prefix.dev/conda-forge/"
);
}
}
4 changes: 0 additions & 4 deletions crates/rattler_networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@ pub mod authentication_storage;
pub mod mirror_middleware;
pub mod oci_middleware;
pub mod retry_policies;

mod redaction;

pub use redaction::{redact_known_secrets_from_url, Redact, DEFAULT_REDACTION_STR};
1 change: 1 addition & 0 deletions crates/rattler_package_streaming/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ num_cpus = { workspace = true }
rattler_conda_types = { path="../rattler_conda_types", version = "0.26.3", default-features = false }
rattler_digest = { path="../rattler_digest", version = "1.0.0", default-features = false }
rattler_networking = { path="../rattler_networking", version = "0.20.10", default-features = false }
rattler_redaction = { path="../rattler_redaction", features = ["reqwest", "reqwest-middleware"] }
reqwest = { workspace = true, features = ["stream"], optional = true }
reqwest-middleware = { workspace = true, optional = true }
serde_json = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler_package_streaming/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use zip::result::ZipError;
use rattler_digest::{Md5Hash, Sha256Hash};

#[cfg(feature = "reqwest")]
use rattler_networking::Redact;
use rattler_redaction::Redact;

pub mod read;
pub mod seek;
Expand Down
16 changes: 16 additions & 0 deletions crates/rattler_redaction/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rattler_redaction"
version = "0.1.0"
edition.workspace = true
authors = ["Wolf Vollprecht <w.vollprecht@gmail.com>"]
description = "Redact sensitive information from URLs (ie. conda tokens)"
categories.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
readme.workspace = true

[dependencies]
url = { workspace = true }
reqwest = { workspace = true, optional = true }
reqwest-middleware = { workspace = true, optional = true }
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use itertools::Itertools;
use url::Url;

/// A default string to use for redaction.
Expand All @@ -15,28 +14,41 @@ pub const DEFAULT_REDACTION_STR: &str = "********";
/// # Example
///
/// ```rust
/// # use rattler_networking::{redact_known_secrets_from_url, DEFAULT_REDACTION_STR};
/// # use rattler_redaction::{redact_known_secrets_from_url, Redact, DEFAULT_REDACTION_STR};
/// # use url::Url;
///
/// let url = Url::parse("https://conda.anaconda.org/t/12345677/conda-forge/noarch/repodata.json").unwrap();
/// let redacted_url = redact_known_secrets_from_url(&url, DEFAULT_REDACTION_STR).unwrap_or(url);
/// let redacted_url = redact_known_secrets_from_url(&url, DEFAULT_REDACTION_STR).unwrap_or(url.clone());
/// // or you can use the shorthand
/// let redacted_url = url.redact();
/// ```
pub fn redact_known_secrets_from_url(url: &Url, redaction: &str) -> Option<Url> {
let mut url = url.clone();
if url.password().is_some() {
url.set_password(Some(redaction)).ok()?;
}

let mut segments = url.path_segments()?;
match (segments.next(), segments.next()) {
(Some("t"), Some(_)) => {
let remainder = segments.collect_vec();
let redacted_path = format!(
"t/{redaction}{seperator}{remainder}",
seperator = if remainder.is_empty() { "" } else { "/" },
remainder = remainder.iter().format("/")
let remainder = segments.collect::<Vec<_>>();
let mut redacted_path = format!(
"t/{redaction}{separator}",
separator = if remainder.is_empty() { "" } else { "/" },
);

let mut url = url.clone();
for (idx, segment) in remainder.iter().enumerate() {
redacted_path.push_str(segment);
// if the original url ends with a slash, we need to add it to the redacted path
if idx < remainder.len() - 1 {
redacted_path.push('/');
}
}

url.set_path(&redacted_path);
Some(url)
}
_ => None,
_ => Some(url),
}
}

Expand All @@ -46,6 +58,7 @@ pub trait Redact {
fn redact(self) -> Self;
}

#[cfg(feature = "reqwest-middleware")]
impl Redact for reqwest_middleware::Error {
fn redact(self) -> Self {
if let Some(url) = self.url() {
Expand All @@ -58,6 +71,7 @@ impl Redact for reqwest_middleware::Error {
}
}

#[cfg(feature = "reqwest")]
impl Redact for reqwest::Error {
fn redact(self) -> Self {
if let Some(url) = self.url() {
Expand Down Expand Up @@ -99,13 +113,37 @@ mod test {
)
);

// should stay as is
assert_eq!(
redact_known_secrets_from_url(
&Url::from_str("https://conda.anaconda.org/conda-forge/noarch/repodata.json")
.unwrap(),
"helloworld"
),
None,
)
.unwrap(),
Url::from_str("https://conda.anaconda.org/conda-forge/noarch/repodata.json").unwrap(),
);

let redacted = redact_known_secrets_from_url(
&Url::from_str("https://user:secret@prefix.dev/conda-forge").unwrap(),
DEFAULT_REDACTION_STR,
)
.unwrap();

assert_eq!(
redacted.to_string(),
format!("https://user:{DEFAULT_REDACTION_STR}@prefix.dev/conda-forge")
);

let redacted = redact_known_secrets_from_url(
&Url::from_str("https://user:secret@prefix.dev/conda-forge/").unwrap(),
DEFAULT_REDACTION_STR,
)
.unwrap();

assert_eq!(
redacted.to_string(),
format!("https://user:{DEFAULT_REDACTION_STR}@prefix.dev/conda-forge/")
);
}
}
1 change: 1 addition & 0 deletions crates/rattler_repodata_gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }
zstd = { workspace = true }
rattler_cache = { version = "0.1.4", path = "../rattler_cache" }
rattler_redaction = { version = "0.1.0", path = "../rattler_redaction", features = ["reqwest", "reqwest-middleware"] }

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler_repodata_gateway/src/fetch/jlap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ use blake2::digest::{FixedOutput, Update};
use rattler_digest::{
parse_digest_from_hex, serde::SerializableHash, Blake2b256, Blake2b256Hash, Blake2bMac256,
};
use rattler_networking::Redact;
use rattler_redaction::Redact;
use reqwest::{
header::{HeaderMap, HeaderValue},
Response, StatusCode,
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler_repodata_gateway/src/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use cache_control::{Cachability, CacheControl};
use futures::{future::ready, FutureExt, TryStreamExt};
use humansize::{SizeFormatter, DECIMAL};
use rattler_digest::{compute_file_digest, Blake2b256, HashingWriter};
use rattler_networking::Redact;
use rattler_redaction::Redact;
use reqwest::{
header::{HeaderMap, HeaderValue},
Response, StatusCode,
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler_repodata_gateway/src/gateway/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::fetch;
use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError};
use crate::gateway::direct_url_query::DirectUrlQueryError;
use rattler_conda_types::{Channel, MatchSpec};
use rattler_networking::Redact;
use rattler_redaction::Redact;
use reqwest_middleware::Error;
use simple_spawn_blocking::Cancelled;
use std::fmt::{Display, Formatter};
Expand Down
Loading