mirror of https://github.com/astral-sh/uv
Avoid writing redacted credentials to tool receipt (#14855)
## Summary Right now, we write index URLs to the tool receipt with redacted credentials (i.e., a username, and `****` in lieu of a password). This is always wrong and unusable. Instead, this PR drops them entirely. Part of https://github.com/astral-sh/uv/issues/14806.
This commit is contained in:
parent
09549c2e71
commit
4dd0392086
|
|
@ -203,7 +203,11 @@ impl serde::ser::Serialize for IndexUrl {
|
|||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
match self {
|
||||
Self::Pypi(url) => url.without_credentials().serialize(serializer),
|
||||
Self::Url(url) => url.without_credentials().serialize(serializer),
|
||||
Self::Path(url) => url.without_credentials().serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +408,9 @@ impl<'a> IndexLocations {
|
|||
} else {
|
||||
let mut indexes = vec![];
|
||||
|
||||
// TODO(charlie): By only yielding the first default URL, we'll drop credentials if,
|
||||
// e.g., an authenticated default URL is provided in a configuration file, but an
|
||||
// unauthenticated default URL is present in the receipt.
|
||||
let mut seen = FxHashSet::default();
|
||||
let mut default = false;
|
||||
for index in {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use ref_cast::RefCast;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
|
@ -98,6 +99,24 @@ impl DisplaySafeUrl {
|
|||
let _ = self.0.set_password(None);
|
||||
}
|
||||
|
||||
/// Returns the URL with any credentials removed.
|
||||
pub fn without_credentials(&self) -> Cow<'_, Url> {
|
||||
if self.0.password().is_none() && self.0.username() == "" {
|
||||
return Cow::Borrowed(&self.0);
|
||||
}
|
||||
|
||||
// For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the
|
||||
// username.
|
||||
if is_ssh_git_username(&self.0) {
|
||||
return Cow::Borrowed(&self.0);
|
||||
}
|
||||
|
||||
let mut url = self.0.clone();
|
||||
let _ = url.set_username("");
|
||||
let _ = url.set_password(None);
|
||||
Cow::Owned(url)
|
||||
}
|
||||
|
||||
/// Returns [`Display`] implementation that doesn't mask credentials.
|
||||
#[inline]
|
||||
pub fn displayable_with_credentials(&self) -> impl Display {
|
||||
|
|
|
|||
|
|
@ -3629,3 +3629,83 @@ fn tool_install_mismatched_name() {
|
|||
error: Package name (`black`) provided with `--from` does not match install request (`flask`)
|
||||
"###);
|
||||
}
|
||||
|
||||
/// When installing from an authenticated index, the credentials should be omitted from the receipt.
|
||||
#[test]
|
||||
fn tool_install_credentials() {
|
||||
let context = TestContext::new("3.12")
|
||||
.with_exclude_newer("2025-01-18T00:00:00Z")
|
||||
.with_filtered_counts()
|
||||
.with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `executable-application`
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("executable-application")
|
||||
.arg("--index")
|
||||
.arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple")
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Prepared [N] packages in [TIME]
|
||||
Installed [N] packages in [TIME]
|
||||
+ executable-application==0.3.0
|
||||
Installed 1 executable: app
|
||||
"###);
|
||||
|
||||
tool_dir
|
||||
.child("executable-application")
|
||||
.assert(predicate::path::is_dir());
|
||||
tool_dir
|
||||
.child("executable-application")
|
||||
.child("uv-receipt.toml")
|
||||
.assert(predicate::path::exists());
|
||||
|
||||
let executable = bin_dir.child(format!("app{}", std::env::consts::EXE_SUFFIX));
|
||||
assert!(executable.exists());
|
||||
|
||||
// On Windows, we can't snapshot an executable file.
|
||||
#[cfg(not(windows))]
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// Should run black in the virtual environment
|
||||
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
||||
#![TEMP_DIR]/tools/executable-application/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from executable_application import main
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[0].endswith("-script.pyw"):
|
||||
sys.argv[0] = sys.argv[0][:-11]
|
||||
elif sys.argv[0].endswith(".exe"):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
"###);
|
||||
|
||||
});
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
{ name = "app", install-path = "[TEMP_DIR]/bin/app" },
|
||||
]
|
||||
|
||||
[tool.options]
|
||||
index = [{ url = "https://pypi-proxy.fly.dev/basic-auth/simple", explicit = false, default = false, format = "simple", authenticate = "auto" }]
|
||||
exclude-newer = "2025-01-18T00:00:00Z"
|
||||
"#);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue