From faa12f50ceea52c37f1f59e35ff704898b9f24b3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 23 Jul 2025 17:23:51 -0400 Subject: [PATCH] Respect credentials from all defined indexes (#14858) ## Summary The core problem here is that `allowed_indexes` only includes at most one "default" index. This is problematic for tool upgrades, since the index in the receipt will be marked as default, but credentials will be omitted; if credentials are then defined in a `uv.toml`, we'll never look at those, since that will _also_ be marked as default, and we only look at the first default. Instead, we should consider all defined indexes in priority order. Closes https://github.com/astral-sh/uv/issues/14806. --- crates/uv-distribution-types/src/index_url.rs | 26 +++- crates/uv/tests/it/tool_install.rs | 123 ++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index bc0fcf59c..a75f1ea1b 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -400,8 +400,8 @@ impl<'a> IndexLocations { /// /// This includes explicit indexes, implicit indexes, flat indexes, and the default index. /// - /// The indexes will be returned in the order in which they were defined, such that the - /// last-defined index is the last item in the vector. + /// The indexes will be returned in the reverse of the order in which they were defined, such + /// that the last-defined index is the first item in the vector. pub fn allowed_indexes(&'a self) -> Vec<&'a Index> { if self.no_index { self.flat_index.iter().rev().collect() @@ -436,9 +436,29 @@ impl<'a> IndexLocations { } } + /// Return a vector containing all known [`Index`] entries. + /// + /// This includes explicit indexes, implicit indexes, flat indexes, and default indexes; + /// in short, it includes all defined indexes, even if they're overridden by some other index + /// definition. + /// + /// The indexes will be returned in the reverse of the order in which they were defined, such + /// that the last-defined index is the first item in the vector. + pub fn known_indexes(&'a self) -> impl Iterator { + if self.no_index { + Either::Left(self.flat_index.iter().rev()) + } else { + Either::Right( + std::iter::once(&*DEFAULT_INDEX) + .chain(self.flat_index.iter().rev()) + .chain(self.indexes.iter().rev()), + ) + } + } + /// Add all authenticated sources to the cache. pub fn cache_index_credentials(&self) { - for index in self.allowed_indexes() { + for index in self.known_indexes() { if let Some(credentials) = index.credentials() { let credentials = Arc::new(credentials); uv_auth::store_credentials(index.raw_url(), credentials.clone()); diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 00cc17c04..0af2510fb 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -3709,3 +3709,126 @@ fn tool_install_credentials() { "#); }); } + +/// When installing from an authenticated index, the credentials should be omitted from the receipt. +#[test] +fn tool_install_default_credentials() -> Result<()> { + 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"); + + // Write a `uv.toml` with a default index that has credentials. + let uv_toml = context.temp_dir.child("uv.toml"); + uv_toml.write_str(indoc::indoc! {r#" + [[index]] + url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple" + default = true + authenticate = "always" + "#})?; + + // Install `executable-application` + uv_snapshot!(context.filters(), context.tool_install() + .arg("executable-application") + .arg("--config-file") + .arg(uv_toml.as_os_str()) + .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 = true, format = "simple", authenticate = "always" }] + exclude-newer = "2025-01-18T00:00:00Z" + "#); + }); + + // Attempt to upgrade without providing the credentials (from the config file). + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("executable-application") + .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: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: Failed to upgrade executable-application + Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/simple/executable-application/` + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/simple/executable-application/ + "); + + // Attempt to upgrade. + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("executable-application") + .arg("--config-file") + .arg(uv_toml.as_os_str()) + .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 ----- + Nothing to upgrade + "); + + Ok(()) +}