diff --git a/Cargo.lock b/Cargo.lock index d338b35442ad..eb92cb521642 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1017,6 +1017,7 @@ dependencies = [ "tracing", "url", "urlencoding", + "uv-auth", "uv-cache-info", "uv-fs", "uv-git", diff --git a/crates/distribution-types/Cargo.toml b/crates/distribution-types/Cargo.toml index 714b14d51ab7..d970cf6c32d0 100644 --- a/crates/distribution-types/Cargo.toml +++ b/crates/distribution-types/Cargo.toml @@ -19,6 +19,7 @@ pep440_rs = { workspace = true } pep508_rs = { workspace = true, features = ["serde"] } platform-tags = { workspace = true } pypi-types = { workspace = true } +uv-auth = { workspace = true } uv-cache-info = { workspace = true } uv-fs = { workspace = true } uv-git = { workspace = true } diff --git a/crates/distribution-types/src/index.rs b/crates/distribution-types/src/index.rs index 4d335a8f7302..4ffafebcc4de 100644 --- a/crates/distribution-types/src/index.rs +++ b/crates/distribution-types/src/index.rs @@ -2,6 +2,7 @@ use crate::{IndexUrl, IndexUrlError}; use std::str::FromStr; use thiserror::Error; use url::Url; +use uv_auth::Credentials; #[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -102,6 +103,19 @@ impl Index { pub fn raw_url(&self) -> &Url { self.url.url() } + + /// Retrieve the credentials for the index, either from the environment, or from the URL itself. + pub fn credentials(&self) -> Option { + // If the index is named, and credentials are provided via the environment, prefer those. + if let Some(name) = self.name.as_deref() { + if let Some(credentials) = Credentials::from_env(name) { + return Some(credentials); + } + } + + // Otherwise, extract the credentials from the URL. + Credentials::from_url(self.url.url()) + } } impl FromStr for Index { diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index c6dcc0ea0ee6..d8c4413da3c5 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -407,7 +407,7 @@ impl<'a> IndexLocations { } /// Return an iterator over the [`FlatIndexLocation`] entries. - pub fn flat_index(&'a self) -> impl Iterator + 'a { + pub fn flat_indexes(&'a self) -> impl Iterator + 'a { self.flat_index.iter() } @@ -424,9 +424,10 @@ impl<'a> IndexLocations { } } - /// Return an iterator over all allowed [`IndexUrl`] entries. + /// Return an iterator over all allowed [`Index`] entries. /// - /// This includes both explicit and implicit indexes, as well as the default index. + /// This includes both explicit and implicit indexes, as well as the default index (but _not_ + /// the flat indexes). /// /// If `no_index` was enabled, then this always returns an empty /// iterator. @@ -435,18 +436,6 @@ impl<'a> IndexLocations { .chain(self.implicit_indexes()) .chain(self.default_index()) } - - /// Return an iterator over all allowed [`Url`] entries. - /// - /// This includes both explicit and implicit index URLs, as well as the default index. - /// - /// If `no_index` was enabled, then this always returns an empty - /// iterator. - pub fn allowed_urls(&'a self) -> impl Iterator + 'a { - self.allowed_indexes() - .map(Index::raw_url) - .chain(self.flat_index().map(FlatIndexLocation::url)) - } } /// The index URLs to use for fetching packages. diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index 0c301dcecae4..9776fca9124c 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -139,6 +139,21 @@ impl Credentials { }) } + /// Extract the [`Credentials`] from the environment, given a named source. + /// + /// For example, given a name of `"pytorch"`, search for `UV_HTTP_BASIC_PYTORCH_USERNAME` and + /// `UV_HTTP_BASIC_PYTORCH_PASSWORD`. + pub fn from_env(name: &str) -> Option { + let name = name.to_uppercase(); + let username = std::env::var(format!("UV_HTTP_BASIC_{name}_USERNAME")).ok(); + let password = std::env::var(format!("UV_HTTP_BASIC_{name}_PASSWORD")).ok(); + if username.is_none() && password.is_none() { + None + } else { + Some(Self::new(username, password)) + } + } + /// Parse [`Credentials`] from an HTTP request, if any. /// /// Only HTTP Basic Authentication is supported. diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 61c1c282566a..16f644418697 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -35,3 +35,11 @@ pub fn store_credentials_from_url(url: &Url) -> bool { false } } + +/// Populate the global authentication store with credentials on a URL, if there are any. +/// +/// Returns `true` if the store was updated. +pub fn store_credentials(url: &Url, credentials: Credentials) { + trace!("Caching credentials for {url}"); + CREDENTIALS_CACHE.insert(url, Arc::new(credentials)); +} diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 1e3e1d7e226d..e395ab8ec4fb 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -89,7 +89,7 @@ impl<'a> RegistryWheelIndex<'a> { // Collect into owned `IndexUrl`. let flat_index_urls: Vec = index_locations - .flat_index() + .flat_indexes() .map(|flat_index| Index::from_extra_index_url(IndexUrl::from(flat_index.clone()))) .collect(); diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index a238c16b3f1c..ac9717cc6ef8 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1055,7 +1055,7 @@ impl Lock { }) .chain( locations - .flat_index() + .flat_indexes() .filter_map(|index_url| match index_url { FlatIndexLocation::Url(_) => { Some(UrlString::from(index_url.redacted())) @@ -1081,7 +1081,7 @@ impl Lock { }) .chain( locations - .flat_index() + .flat_indexes() .filter_map(|index_url| match index_url { FlatIndexLocation::Url(_) => None, FlatIndexLocation::Path(index_url) => { diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index b4922cb9331f..0f89d636d315 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -621,7 +621,7 @@ impl PubGrubReportFormatter<'_> { incomplete_packages: &FxHashMap>, hints: &mut IndexSet, ) { - let no_find_links = index_locations.flat_index().peekable().peek().is_none(); + let no_find_links = index_locations.flat_indexes().peekable().peek().is_none(); // Add hints due to the package being entirely unavailable. match unavailable_packages.get(name) { diff --git a/crates/uv/src/commands/build.rs b/crates/uv/src/commands/build.rs index 397689d61315..ddb0cbc03548 100644 --- a/crates/uv/src/commands/build.rs +++ b/crates/uv/src/commands/build.rs @@ -8,7 +8,7 @@ use distribution_types::{DependencyMetadata, IndexLocations}; use install_wheel_rs::linker::LinkMode; use owo_colors::OwoColorize; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -391,8 +391,13 @@ async fn build_package( .into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Read build constraints. @@ -445,7 +450,7 @@ async fn build_package( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, None, &hasher, build_options) }; diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 995b254d1265..b457d6b35b83 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -13,7 +13,7 @@ use distribution_types::{ }; use install_wheel_rs::linker::LinkMode; use pypi_types::{Requirement, SupportedEnvironments}; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -285,8 +285,13 @@ pub(crate) async fn pip_compile( ); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -309,7 +314,7 @@ pub(crate) async fn pip_compile( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, &cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, tags.as_deref(), &hasher, &build_options) }; @@ -459,7 +464,7 @@ pub(crate) async fn pip_compile( // If necessary, include the `--find-links` locations. if include_find_links { - for flat_index in index_locations.flat_index() { + for flat_index in index_locations.flat_indexes() { writeln!(writer, "--find-links {}", flat_index.verbatim())?; wrote_preamble = true; } diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index b09741455d18..81dfc09a2e2c 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -12,7 +12,7 @@ use distribution_types::{ use install_wheel_rs::linker::LinkMode; use pep508_rs::PackageName; use pypi_types::Requirement; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -283,8 +283,13 @@ pub(crate) async fn pip_install( ); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -302,7 +307,7 @@ pub(crate) async fn pip_install( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, &cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(&tags), &hasher, &build_options) }; diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 562131cba6cd..54a1067c9c31 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -8,7 +8,7 @@ use tracing::debug; use distribution_types::{DependencyMetadata, Index, IndexLocations, Resolution}; use install_wheel_rs::linker::LinkMode; use pep508_rs::PackageName; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -226,8 +226,13 @@ pub(crate) async fn pip_sync( ); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -245,7 +250,7 @@ pub(crate) async fn pip_sync( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, &cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(&tags), &hasher, &build_options) }; diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 76585eb4bc4e..4c3ed74edc42 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -12,7 +12,7 @@ use cache_key::RepositoryUrl; use distribution_types::UnresolvedRequirement; use pep508_rs::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl}; use pypi_types::{redact_git_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl}; -use uv_auth::{store_credentials_from_url, Credentials}; +use uv_auth::{store_credentials, store_credentials_from_url, Credentials}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -242,8 +242,13 @@ pub(crate) async fn add( resolution_environment(python_version, python_platform, target.interpreter())?; // Add all authenticated sources to the cache. - for url in settings.index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in settings.index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in settings.index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -272,7 +277,9 @@ pub(crate) async fn add( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(settings.index_locations.flat_index()).await?; + let entries = client + .fetch(settings.index_locations.flat_indexes()) + .await?; FlatIndex::from_entries(entries, Some(&tags), &hasher, &settings.build_options) }; diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index f26dd470f703..5fff6cac084c 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -14,7 +14,7 @@ use distribution_types::{ }; use pep440_rs::Version; use pypi_types::{Requirement, SupportedEnvironments}; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -354,8 +354,13 @@ async fn do_lock( PythonRequirement::from_requires_python(interpreter, requires_python.clone()); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -399,7 +404,7 @@ async fn do_lock( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, None, &hasher, build_options) }; diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 53d2dfef16f4..cd2e6c97bc65 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -11,7 +11,7 @@ use distribution_types::{ use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::MarkerTreeContents; use pypi_types::Requirement; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{Concurrency, Constraints, ExtrasSpecification, Reinstall, Upgrade}; @@ -626,8 +626,13 @@ pub(crate) async fn resolve_names( } = settings; // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -774,8 +779,13 @@ pub(crate) async fn resolve_environment<'a>( let python_requirement = PythonRequirement::from_interpreter(interpreter); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -837,7 +847,7 @@ pub(crate) async fn resolve_environment<'a>( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; @@ -933,8 +943,13 @@ pub(crate) async fn sync_environment( let markers = interpreter.resolver_markers(); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -968,7 +983,7 @@ pub(crate) async fn sync_environment( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; @@ -1122,8 +1137,13 @@ pub(crate) async fn update_environment( } // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Initialize the registry client. @@ -1171,7 +1191,7 @@ pub(crate) async fn update_environment( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; @@ -1351,7 +1371,7 @@ fn warn_on_requirements_txt_setting( } } for find_link in find_links { - if !settings.index_locations.flat_index().contains(find_link) { + if !settings.index_locations.flat_indexes().contains(find_link) { warn_user_once!( "Ignoring `--find-links` from requirements file: `{find_link}`. Instead, use the `--find-links` command-line argument, or set `find-links` in a `uv.toml` or `pyproject.toml` file.`" ); diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index b663b0fde413..b5eb7e06b390 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -15,6 +15,7 @@ use pypi_types::{ use std::borrow::Cow; use std::path::Path; use std::str::FromStr; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -254,8 +255,13 @@ pub(super) async fn do_sync( let resolution = apply_editable_mode(resolution, editable); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - uv_auth::store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Populate credentials from the workspace. @@ -294,7 +300,7 @@ pub(super) async fn do_sync( // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); - let entries = client.fetch(index_locations.flat_index()).await?; + let entries = client.fetch(index_locations.flat_indexes()).await?; FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index af963e47c430..2df6f886376e 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -12,7 +12,7 @@ use thiserror::Error; use distribution_types::{DependencyMetadata, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pypi_types::Requirement; -use uv_auth::store_credentials_from_url; +use uv_auth::{store_credentials, store_credentials_from_url}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -226,8 +226,13 @@ async fn venv_impl( let interpreter = python.into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } if managed { @@ -275,8 +280,13 @@ async fn venv_impl( let interpreter = venv.interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.allowed_urls() { - store_credentials_from_url(url); + for index in index_locations.allowed_indexes() { + if let Some(credentials) = index.credentials() { + store_credentials(index.raw_url(), credentials); + } + } + for index in index_locations.flat_indexes() { + store_credentials_from_url(index.url()); } // Instantiate a client. @@ -296,7 +306,7 @@ async fn venv_impl( let tags = interpreter.tags().map_err(VenvError::Tags)?; let client = FlatIndexClient::new(&client, cache); let entries = client - .fetch(index_locations.flat_index()) + .fetch(index_locations.flat_indexes()) .await .map_err(VenvError::FlatIndex)?; FlatIndex::from_entries( diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index a220792f8618..2f55f8604a83 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -6223,6 +6223,94 @@ fn lock_redact_git_sources() -> Result<()> { Ok(()) } +/// Pass credentials for a named index via environment variables. +#[test] +fn lock_env_credentials() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.index]] + name = "proxy" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true + "#, + )?; + + // Without credentials, the resolution should fail. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + "###); + + // Provide credentials via environment variables. + uv_snapshot!(context.filters(), context.lock() + .env("UV_HTTP_BASIC_PROXY_USERNAME", "public") + .env("UV_HTTP_BASIC_PROXY_PASSWORD", "heron"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + // The lockfile shout omit the credentials. + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "foo" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + "### + ); + }); + + Ok(()) +} + /// Resolve against an index that uses relative links. #[test] fn lock_relative_index() -> Result<()> { diff --git a/docs/configuration/indexes.md b/docs/configuration/indexes.md index 70ef968f5f9a..df3c1403363a 100644 --- a/docs/configuration/indexes.md +++ b/docs/configuration/indexes.md @@ -95,6 +95,45 @@ Users can opt in to alternate index behaviors via the`--index-strategy` command- While `unsafe-best-match` is the closest to pip's behavior, it exposes users to the risk of "dependency confusion" attacks. +## Providing credentials + +Most private registries require authentication to access packages, typically via a username and +password (or access token). + +To authenticate with a provide index, either provide credentials via environment variables or embed +them in the URL. + +For example, given an index named `internal` that requires a username (`public`) and password +(`koala`), define the index (without credentials) in your `pyproject.toml`: + +```toml +[[tool.uv.index]] +name = "internal" +url = "https://pypi-proxy.corp.dev/simple" +``` + +From there, you can set the `UV_INDEX_INTERNAL_USERNAME` and `UV_INDEX_INTERNAL_PASSWORD` +environment variables, where `INTERNAL` is the uppercase version of the index name: + +```sh +export UV_INDEX_INTERNAL_USERNAME=public +export UV_INDEX_INTERNAL_PASSWORD=koala +``` + +By providing credentials via environment variables, you can avoid storing sensitive information in +the plaintext `pyproject.toml` file. + +Alternatively, credentials can be embedded directly in the index definition: + +```toml +[[tool.uv.index]] +name = "internal" +url = "https://public:koala@https://pypi-proxy.corp.dev/simple" +``` + +For security purposes, credentials are _never_ stored in the `uv.lock` file; as such, uv _must_ have +access to the authenticated URL at installation time. + ## `--index-url` and `--extra-index-url` In addition to the `[[tool.uv.index]]` configuration option, uv supports pip-style `--index-url` and