mirror of https://github.com/astral-sh/uv
Redact credentials when displaying URLs (#13333)
This PR redacts credentials in displayed URLs. It mostly relies on a `redacted_url` function (and where possible `IndexUrl::redacted`). This is a quick way to prevent leaked credentials but it's prone to programmer error when adding new trace statements. A better follow-on would use a `RedactedUrl` type with the appropriate `Display` implementation. This would allow us to still extract credentials from the URL while displaying it securely. On the plus side, the sites where the `redacted_url` function are used serve as easy signposts for where to use the new type in a future PR. Closes #1714.
This commit is contained in:
parent
1afadda819
commit
6df588bb00
|
|
@ -4664,6 +4664,7 @@ dependencies = [
|
||||||
"uv-publish",
|
"uv-publish",
|
||||||
"uv-pypi-types",
|
"uv-pypi-types",
|
||||||
"uv-python",
|
"uv-python",
|
||||||
|
"uv-redacted",
|
||||||
"uv-requirements",
|
"uv-requirements",
|
||||||
"uv-requirements-txt",
|
"uv-requirements-txt",
|
||||||
"uv-resolver",
|
"uv-resolver",
|
||||||
|
|
@ -4950,6 +4951,7 @@ dependencies = [
|
||||||
"uv-pep508",
|
"uv-pep508",
|
||||||
"uv-platform-tags",
|
"uv-platform-tags",
|
||||||
"uv-pypi-types",
|
"uv-pypi-types",
|
||||||
|
"uv-redacted",
|
||||||
"uv-small-str",
|
"uv-small-str",
|
||||||
"uv-static",
|
"uv-static",
|
||||||
"uv-torch",
|
"uv-torch",
|
||||||
|
|
@ -5250,6 +5252,7 @@ dependencies = [
|
||||||
"uv-cache-key",
|
"uv-cache-key",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-git-types",
|
"uv-git-types",
|
||||||
|
"uv-redacted",
|
||||||
"uv-static",
|
"uv-static",
|
||||||
"uv-version",
|
"uv-version",
|
||||||
"which",
|
"which",
|
||||||
|
|
@ -5263,6 +5266,7 @@ dependencies = [
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uv-redacted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5597,6 +5601,13 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv-redacted"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uv-requirements"
|
name = "uv-requirements"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ uv-platform-tags = { path = "crates/uv-platform-tags" }
|
||||||
uv-publish = { path = "crates/uv-publish" }
|
uv-publish = { path = "crates/uv-publish" }
|
||||||
uv-pypi-types = { path = "crates/uv-pypi-types" }
|
uv-pypi-types = { path = "crates/uv-pypi-types" }
|
||||||
uv-python = { path = "crates/uv-python" }
|
uv-python = { path = "crates/uv-python" }
|
||||||
|
uv-redacted = { path = "crates/uv-redacted" }
|
||||||
uv-requirements = { path = "crates/uv-requirements" }
|
uv-requirements = { path = "crates/uv-requirements" }
|
||||||
uv-requirements-txt = { path = "crates/uv-requirements-txt" }
|
uv-requirements-txt = { path = "crates/uv-requirements-txt" }
|
||||||
uv-resolver = { path = "crates/uv-resolver" }
|
uv-resolver = { path = "crates/uv-resolver" }
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,8 @@ impl From<(Realm, Username)> for RealmUsername {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::credentials::Password;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -331,4 +333,23 @@ mod tests {
|
||||||
let url = Url::parse("https://example.com/foobar").unwrap();
|
let url = Url::parse("https://example.com/foobar").unwrap();
|
||||||
assert_eq!(trie.get(&url), None);
|
assert_eq!(trie.get(&url), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_url_with_credentials() {
|
||||||
|
let username = Username::new(Some(String::from("username")));
|
||||||
|
let password = Password::new(String::from("password"));
|
||||||
|
let credentials = Arc::new(Credentials::Basic {
|
||||||
|
username: username.clone(),
|
||||||
|
password: Some(password),
|
||||||
|
});
|
||||||
|
let cache = CredentialsCache::default();
|
||||||
|
// Insert with URL with credentials and get with redacted URL.
|
||||||
|
let url = Url::parse("https://username:password@example.com/foobar").unwrap();
|
||||||
|
cache.insert(&url, credentials.clone());
|
||||||
|
assert_eq!(cache.get_url(&url, &username), Some(credentials.clone()));
|
||||||
|
// Insert with redacted URL and get with URL with credentials.
|
||||||
|
let url = Url::parse("https://username:password@second-example.com/foobar").unwrap();
|
||||||
|
cache.insert(&url, credentials.clone());
|
||||||
|
assert_eq!(cache.get_url(&url, &username), Some(credentials.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ uv-pep508 = { workspace = true }
|
||||||
uv-platform-tags = { workspace = true }
|
uv-platform-tags = { workspace = true }
|
||||||
uv-pypi-types = { workspace = true }
|
uv-pypi-types = { workspace = true }
|
||||||
uv-small-str = { workspace = true }
|
uv-small-str = { workspace = true }
|
||||||
|
uv-redacted = { workspace = true }
|
||||||
uv-static = { workspace = true }
|
uv-static = { workspace = true }
|
||||||
uv-torch = { workspace = true }
|
uv-torch = { workspace = true }
|
||||||
uv-version = { workspace = true }
|
uv-version = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use url::Url;
|
||||||
|
|
||||||
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
|
|
||||||
use crate::middleware::OfflineError;
|
use crate::middleware::OfflineError;
|
||||||
use crate::{html, FlatIndexError};
|
use crate::{html, FlatIndexError};
|
||||||
|
|
@ -197,10 +198,10 @@ pub enum ErrorKind {
|
||||||
#[error("Failed to fetch: `{0}`")]
|
#[error("Failed to fetch: `{0}`")]
|
||||||
WrappedReqwestError(Url, #[source] WrappedReqwestError),
|
WrappedReqwestError(Url, #[source] WrappedReqwestError),
|
||||||
|
|
||||||
#[error("Received some unexpected JSON from {url}")]
|
#[error("Received some unexpected JSON from {}", redacted_url(url))]
|
||||||
BadJson { source: serde_json::Error, url: Url },
|
BadJson { source: serde_json::Error, url: Url },
|
||||||
|
|
||||||
#[error("Received some unexpected HTML from {url}")]
|
#[error("Received some unexpected HTML from {}", redacted_url(url))]
|
||||||
BadHtml { source: html::Error, url: Url },
|
BadHtml { source: html::Error, url: Url },
|
||||||
|
|
||||||
#[error("Failed to read zip with range requests: `{0}`")]
|
#[error("Failed to read zip with range requests: `{0}`")]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use uv_cache_key::cache_digest;
|
||||||
use uv_distribution_filename::DistFilename;
|
use uv_distribution_filename::DistFilename;
|
||||||
use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString};
|
use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString};
|
||||||
use uv_pypi_types::HashDigests;
|
use uv_pypi_types::HashDigests;
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
use uv_small_str::SmallString;
|
use uv_small_str::SmallString;
|
||||||
|
|
||||||
use crate::cached_client::{CacheControl, CachedClientError};
|
use crate::cached_client::{CacheControl, CachedClientError};
|
||||||
|
|
@ -207,7 +208,7 @@ impl<'a> FlatIndexClient<'a> {
|
||||||
Ok(file) => Some(file),
|
Ok(file) => Some(file),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Ignore files with unparsable version specifiers.
|
// Ignore files with unparsable version specifiers.
|
||||||
warn!("Skipping file in {url}: {err}");
|
warn!("Skipping file in {}: {err}", redacted_url(&url));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ use uv_pep440::Version;
|
||||||
use uv_pep508::MarkerEnvironment;
|
use uv_pep508::MarkerEnvironment;
|
||||||
use uv_platform_tags::Platform;
|
use uv_platform_tags::Platform;
|
||||||
use uv_pypi_types::{ResolutionMetadata, SimpleJson};
|
use uv_pypi_types::{ResolutionMetadata, SimpleJson};
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
use uv_small_str::SmallString;
|
use uv_small_str::SmallString;
|
||||||
use uv_torch::TorchStrategy;
|
use uv_torch::TorchStrategy;
|
||||||
|
|
||||||
|
|
@ -484,7 +485,10 @@ impl RegistryClient {
|
||||||
// ref https://github.com/servo/rust-url/issues/333
|
// ref https://github.com/servo/rust-url/issues/333
|
||||||
.push("");
|
.push("");
|
||||||
|
|
||||||
trace!("Fetching metadata for {package_name} from {url}");
|
trace!(
|
||||||
|
"Fetching metadata for {package_name} from {}",
|
||||||
|
redacted_url(&url)
|
||||||
|
);
|
||||||
|
|
||||||
let cache_entry = self.cache.entry(
|
let cache_entry = self.cache.entry(
|
||||||
CacheBucket::Simple,
|
CacheBucket::Simple,
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,5 @@ serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
||||||
|
uv-redacted = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ pub use crate::reference::GitReference;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
|
|
||||||
mod github;
|
mod github;
|
||||||
mod oid;
|
mod oid;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
@ -151,6 +153,6 @@ impl From<GitUrl> for Url {
|
||||||
|
|
||||||
impl std::fmt::Display for GitUrl {
|
impl std::fmt::Display for GitUrl {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.repository)
|
write!(f, "{}", redacted_url(&self.repository))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ uv-auth = { workspace = true }
|
||||||
uv-cache-key = { workspace = true }
|
uv-cache-key = { workspace = true }
|
||||||
uv-fs = { workspace = true, features = ["tokio"] }
|
uv-fs = { workspace = true, features = ["tokio"] }
|
||||||
uv-git-types = { workspace = true }
|
uv-git-types = { workspace = true }
|
||||||
|
uv-redacted = { workspace = true }
|
||||||
uv-static = { workspace = true }
|
uv-static = { workspace = true }
|
||||||
uv-version = { workspace = true }
|
uv-version = { workspace = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use tracing::trace;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uv_auth::Credentials;
|
use uv_auth::Credentials;
|
||||||
use uv_cache_key::RepositoryUrl;
|
use uv_cache_key::RepositoryUrl;
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
|
|
||||||
/// Global authentication cache for a uv invocation.
|
/// Global authentication cache for a uv invocation.
|
||||||
///
|
///
|
||||||
|
|
@ -31,7 +32,7 @@ impl GitStore {
|
||||||
/// Returns `true` if the store was updated.
|
/// Returns `true` if the store was updated.
|
||||||
pub fn store_credentials_from_url(url: &Url) -> bool {
|
pub fn store_credentials_from_url(url: &Url) -> bool {
|
||||||
if let Some(credentials) = Credentials::from_url(url) {
|
if let Some(credentials) = Credentials::from_url(url) {
|
||||||
trace!("Caching credentials for {url}");
|
trace!("Caching credentials for {}", redacted_url(url));
|
||||||
GIT_STORE.insert(RepositoryUrl::new(url), credentials);
|
GIT_STORE.insert(RepositoryUrl::new(url), credentials);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use url::Url;
|
||||||
|
|
||||||
use uv_cache_key::{cache_digest, RepositoryUrl};
|
use uv_cache_key::{cache_digest, RepositoryUrl};
|
||||||
use uv_git_types::GitUrl;
|
use uv_git_types::GitUrl;
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
|
|
||||||
use crate::git::GitRemote;
|
use crate::git::GitRemote;
|
||||||
use crate::GIT_STORE;
|
use crate::GIT_STORE;
|
||||||
|
|
@ -100,7 +101,10 @@ impl GitSource {
|
||||||
// situation that we have a locked revision but the database
|
// situation that we have a locked revision but the database
|
||||||
// doesn't have it.
|
// doesn't have it.
|
||||||
(locked_rev, db) => {
|
(locked_rev, db) => {
|
||||||
debug!("Updating Git source `{}`", self.git.repository());
|
debug!(
|
||||||
|
"Updating Git source `{}`",
|
||||||
|
redacted_url(self.git.repository())
|
||||||
|
);
|
||||||
|
|
||||||
// Report the checkout operation to the reporter.
|
// Report the checkout operation to the reporter.
|
||||||
let task = self.reporter.as_ref().map(|reporter| {
|
let task = self.reporter.as_ref().map(|reporter| {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "uv-redacted"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
homepage = { workspace = true }
|
||||||
|
documentation = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
url = { workspace = true }
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Return a version of the URL with redacted credentials, allowing the generic `git` username (without a password)
|
||||||
|
/// in SSH URLs, as in, `ssh://git@github.com/...`.
|
||||||
|
pub fn redacted_url(url: &Url) -> Cow<'_, Url> {
|
||||||
|
if url.username().is_empty() && url.password().is_none() {
|
||||||
|
return Cow::Borrowed(url);
|
||||||
|
}
|
||||||
|
if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() {
|
||||||
|
return Cow::Borrowed(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut url = url.clone();
|
||||||
|
let _ = url.set_username("");
|
||||||
|
let _ = url.set_password(None);
|
||||||
|
Cow::Owned(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_no_credentials() {
|
||||||
|
let url = Url::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap();
|
||||||
|
let redacted = redacted_url(&url);
|
||||||
|
assert_eq!(redacted.username(), "");
|
||||||
|
assert!(redacted.password().is_none());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{redacted}"),
|
||||||
|
"https://pypi-proxy.fly.dev/basic-auth/simple"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_username_and_password() {
|
||||||
|
let url = Url::parse("https://user:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap();
|
||||||
|
let redacted = redacted_url(&url);
|
||||||
|
assert_eq!(redacted.username(), "");
|
||||||
|
assert!(redacted.password().is_none());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{redacted}"),
|
||||||
|
"https://pypi-proxy.fly.dev/basic-auth/simple"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_just_password() {
|
||||||
|
let url = Url::parse("https://:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap();
|
||||||
|
let redacted = redacted_url(&url);
|
||||||
|
assert_eq!(redacted.username(), "");
|
||||||
|
assert!(redacted.password().is_none());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{redacted}"),
|
||||||
|
"https://pypi-proxy.fly.dev/basic-auth/simple"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_just_username() {
|
||||||
|
let url = Url::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap();
|
||||||
|
let redacted = redacted_url(&url);
|
||||||
|
assert_eq!(redacted.username(), "");
|
||||||
|
assert!(redacted.password().is_none());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{redacted}"),
|
||||||
|
"https://pypi-proxy.fly.dev/basic-auth/simple"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1515,7 +1515,7 @@ impl std::fmt::Display for PubGrubHint {
|
||||||
"hint".bold().cyan(),
|
"hint".bold().cyan(),
|
||||||
":".bold(),
|
":".bold(),
|
||||||
name.cyan(),
|
name.cyan(),
|
||||||
found_index.cyan(),
|
found_index.redacted().cyan(),
|
||||||
PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(),
|
PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(),
|
||||||
next_index.cyan(),
|
next_index.cyan(),
|
||||||
"--index-strategy unsafe-best-match".green(),
|
"--index-strategy unsafe-best-match".green(),
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ uv-platform-tags = { workspace = true }
|
||||||
uv-publish = { workspace = true }
|
uv-publish = { workspace = true }
|
||||||
uv-pypi-types = { workspace = true }
|
uv-pypi-types = { workspace = true }
|
||||||
uv-python = { workspace = true, features = ["schemars"] }
|
uv-python = { workspace = true, features = ["schemars"] }
|
||||||
|
uv-redacted = { workspace = true }
|
||||||
uv-requirements = { workspace = true }
|
uv-requirements = { workspace = true }
|
||||||
uv-requirements-txt = { workspace = true }
|
uv-requirements-txt = { workspace = true }
|
||||||
uv-resolver = { workspace = true }
|
uv-resolver = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uv_redacted::redacted_url;
|
||||||
|
|
||||||
use crate::commands::human_readable_bytes;
|
use crate::commands::human_readable_bytes;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
@ -300,6 +301,7 @@ impl ProgressReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize {
|
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize {
|
||||||
|
let url = redacted_url(url);
|
||||||
let ProgressMode::Multi {
|
let ProgressMode::Multi {
|
||||||
multi_progress,
|
multi_progress,
|
||||||
state,
|
state,
|
||||||
|
|
@ -330,6 +332,7 @@ impl ProgressReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) {
|
fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) {
|
||||||
|
let url = redacted_url(url);
|
||||||
let ProgressMode::Multi {
|
let ProgressMode::Multi {
|
||||||
state,
|
state,
|
||||||
multi_progress,
|
multi_progress,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue