mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00

Prior to this PR, there were numerous places where uv would leak credentials in logs. We had a way to mask credentials by calling methods or a recently-added `redact_url` function, but this was not secure by default. There were a number of other types (like `GitUrl`) that would leak credentials on display. This PR adds a `DisplaySafeUrl` newtype to prevent leaking credentials when logging by default. It takes a maximalist approach, replacing the use of `Url` almost everywhere. This includes when first parsing config files, when storing URLs in types like `GitUrl`, and also when storing URLs in types that in practice will never contain credentials (like `DirectorySourceUrl`). The idea is to make it easy for developers to do the right thing and for the compiler to support this (and to minimize ever having to manually convert back and forth). Displaying credentials now requires an active step. Note that despite this maximalist approach, the use of the newtype should be zero cost. One conspicuous place this PR does not use `DisplaySafeUrl` is in the `uv-auth` crate. That would require new clones since there are calls to `request.url()` that return a `&Url`. One option would have been to make `DisplaySafeUrl` wrap a `Cow`, but this would lead to lifetime annotations all over the codebase. I've created a separate PR based on this one (#13576) that updates `uv-auth` to use `DisplaySafeUrl` with one new clone. We can discuss the tradeoffs there. Most of this PR just replaces `Url` with `DisplaySafeUrl`. The core is `uv_redacted/lib.rs`, where the newtype is implemented. To make it easier to review the rest, here are some points of note: * `DisplaySafeUrl` has a `Display` implementation that masks credentials. Currently, it will still display the username when there is both a username and password. If we think is the wrong choice, it can now be changed in one place. * `DisplaySafeUrl` has a `remove_credentials()` method and also a `.to_string_with_credentials()` method. This allows us to use it in a variety of scenarios. * `IndexUrl::redacted()` was renamed to `IndexUrl::removed_credentials()` to make it clearer that we are not masking. * We convert from a `DisplaySafeUrl` to a `Url` when calling `reqwest` methods like `.get()` and `.head()`. * We convert from a `DisplaySafeUrl` to a `Url` when creating a `uv_auth::Index`. That is because, as mentioned above, I will be updating the `uv_auth` crate to use this newtype in a separate PR. * A number of tests (e.g., in `pip_install.rs`) that formerly used filters to mask tokens in the test output no longer need those filters since tokens in URLs are now masked automatically. * The one place we are still knowingly writing credentials to `pyproject.toml` is when a URL with credentials is passed to `uv add` with `--raw`. Since displaying credentials is no longer automatic, I have added a `to_string_with_credentials()` method to the `Pep508Url` trait. This is used when `--raw` is passed. Adding it to that trait is a bit weird, but it's the simplest way to achieve the goal. I'm open to suggestions on how to improve this, but note that because of the way we're using generic bounds, it's not as simple as just creating a separate trait for that method.
113 lines
3.4 KiB
Rust
113 lines
3.4 KiB
Rust
use std::fmt::{self, Display, Formatter};
|
|
|
|
use rustc_hash::FxHashSet;
|
|
use url::Url;
|
|
use uv_redacted::DisplaySafeUrl;
|
|
|
|
/// When to use authentication.
|
|
#[derive(
|
|
Copy,
|
|
Clone,
|
|
Debug,
|
|
Default,
|
|
Hash,
|
|
Eq,
|
|
PartialEq,
|
|
Ord,
|
|
PartialOrd,
|
|
serde::Serialize,
|
|
serde::Deserialize,
|
|
)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub enum AuthPolicy {
|
|
/// Authenticate when necessary.
|
|
///
|
|
/// If credentials are provided, they will be used. Otherwise, an unauthenticated request will
|
|
/// be attempted first. If the request fails, uv will search for credentials. If credentials are
|
|
/// found, an authenticated request will be attempted.
|
|
#[default]
|
|
Auto,
|
|
/// Always authenticate.
|
|
///
|
|
/// If credentials are not provided, uv will eagerly search for credentials. If credentials
|
|
/// cannot be found, uv will error instead of attempting an unauthenticated request.
|
|
Always,
|
|
/// Never authenticate.
|
|
///
|
|
/// If credentials are provided, uv will error. uv will not search for credentials.
|
|
Never,
|
|
}
|
|
|
|
impl Display for AuthPolicy {
|
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
match self {
|
|
AuthPolicy::Auto => write!(f, "auto"),
|
|
AuthPolicy::Always => write!(f, "always"),
|
|
AuthPolicy::Never => write!(f, "never"),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(john): We are not using `uv_distribution_types::Index` directly
|
|
// here because it would cause circular crate dependencies. However, this
|
|
// could potentially make sense for a future refactor.
|
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
|
pub struct Index {
|
|
pub url: DisplaySafeUrl,
|
|
/// The root endpoint where authentication is applied.
|
|
/// For PEP 503 endpoints, this excludes `/simple`.
|
|
pub root_url: DisplaySafeUrl,
|
|
pub auth_policy: AuthPolicy,
|
|
}
|
|
|
|
impl Index {
|
|
pub fn is_prefix_for(&self, url: &Url) -> bool {
|
|
if self.root_url.scheme() != url.scheme()
|
|
|| self.root_url.host_str() != url.host_str()
|
|
|| self.root_url.port_or_known_default() != url.port_or_known_default()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
url.path().starts_with(self.root_url.path())
|
|
}
|
|
}
|
|
|
|
// TODO(john): Multiple methods in this struct need to iterate over
|
|
// all the indexes in the set. There are probably not many URLs to
|
|
// iterate through, but we could use a trie instead of a HashSet here
|
|
// for more efficient search.
|
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
|
pub struct Indexes(FxHashSet<Index>);
|
|
|
|
impl Indexes {
|
|
pub fn new() -> Self {
|
|
Self(FxHashSet::default())
|
|
}
|
|
|
|
/// Create a new [`AuthIndexUrls`] from an iterator of [`AuthIndexUrl`]s.
|
|
pub fn from_indexes(urls: impl IntoIterator<Item = Index>) -> Self {
|
|
let mut index_urls = Self::new();
|
|
for url in urls {
|
|
index_urls.0.insert(url);
|
|
}
|
|
index_urls
|
|
}
|
|
|
|
/// Get the index URL prefix for a URL if one exists.
|
|
pub fn index_url_for(&self, url: &Url) -> Option<&DisplaySafeUrl> {
|
|
self.find_prefix_index(url).map(|index| &index.url)
|
|
}
|
|
|
|
/// Get the [`AuthPolicy`] for a URL.
|
|
pub fn auth_policy_for(&self, url: &Url) -> AuthPolicy {
|
|
self.find_prefix_index(url)
|
|
.map(|index| index.auth_policy)
|
|
.unwrap_or(AuthPolicy::Auto)
|
|
}
|
|
|
|
fn find_prefix_index(&self, url: &Url) -> Option<&Index> {
|
|
self.0.iter().find(|&index| index.is_prefix_for(url))
|
|
}
|
|
}
|