Skip to content

Commit

Permalink
fix: redact secrets in the canonical_name functions (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Aug 2, 2024
1 parent ce331ff commit d206d1c
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 29 deletions.
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,
Expand Down Expand Up @@ -54,12 +56,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 @@ -107,10 +120,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 @@ -357,7 +367,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 @@ -667,6 +677,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 @@ -683,7 +715,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/"
);
}

Expand Down
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

0 comments on commit d206d1c

Please sign in to comment.