From de1479c4ef41106d5e87f5ad6ba27b9f94c46b0c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 29 Apr 2025 12:54:07 -0500 Subject: [PATCH] Use index URL instead of package URL for keyring credential lookups (#12651) Some registries (like Azure Artifact) can require you to authenticate separately for every package URL if you do not authenticate for the /simple endpoint. These changes make the auth middleware aware of index URL endpoints and attempts to fetch keyring credentials for such an index URL when making a request to any URL it's a prefix of. The current uv behavior is to cache credentials either at the request URL or realm level. But with these changes, we also need to cache credentials at the index level. Note that when uv does not detect an index URL for a request URL, it will continue to apply the old behavior. Addresses part of #4056 Closes #4583 Closes #11236 Closes #11391 Closes #11507 --- crates/uv-auth/src/cache.rs | 4 +- crates/uv-auth/src/{policy.rs => index.rs} | 63 ++- crates/uv-auth/src/lib.rs | 4 +- crates/uv-auth/src/middleware.rs | 378 +++++++++++++++--- crates/uv-client/src/base_client.rs | 30 +- crates/uv-client/src/registry_client.rs | 21 +- crates/uv-distribution-types/src/index_url.rs | 21 +- crates/uv/src/commands/build_frontend.rs | 4 +- crates/uv/src/commands/pip/compile.rs | 4 +- crates/uv/src/commands/pip/install.rs | 4 +- crates/uv/src/commands/pip/list.rs | 4 +- crates/uv/src/commands/pip/sync.rs | 4 +- crates/uv/src/commands/pip/tree.rs | 4 +- crates/uv/src/commands/project/add.rs | 4 +- crates/uv/src/commands/project/lock.rs | 4 +- crates/uv/src/commands/project/mod.rs | 13 +- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/src/commands/project/tree.rs | 3 +- crates/uv/src/commands/publish.rs | 7 +- crates/uv/src/commands/venv.rs | 4 +- crates/uv/tests/it/edit.rs | 118 ++++++ crates/uv/tests/it/lock.rs | 20 +- crates/uv/tests/it/pip_install.rs | 24 +- crates/uv/tests/it/publish.rs | 18 +- docs/configuration/authentication.md | 6 +- .../keyrings/test_keyring.py | 4 +- 26 files changed, 571 insertions(+), 203 deletions(-) rename crates/uv-auth/src/{policy.rs => index.rs} (51%) diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index 0203efe8b..e8894727b 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -17,8 +17,8 @@ type FxOnceMap = OnceMap>; pub struct CredentialsCache { /// A cache per realm and username realms: RwLock>>, - /// A cache tracking the result of fetches from external services - pub(crate) fetches: FxOnceMap<(Realm, Username), Option>>, + /// A cache tracking the result of realm or index URL fetches from external services + pub(crate) fetches: FxOnceMap<(String, Username), Option>>, /// A cache per URL, uses a trie for efficient prefix queries. urls: RwLock, } diff --git a/crates/uv-auth/src/policy.rs b/crates/uv-auth/src/index.rs similarity index 51% rename from crates/uv-auth/src/policy.rs rename to crates/uv-auth/src/index.rs index 2b6410585..b420c5ec3 100644 --- a/crates/uv-auth/src/policy.rs +++ b/crates/uv-auth/src/index.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Display, Formatter}; -use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; use url::Url; /// When to use authentication. @@ -47,27 +47,45 @@ impl Display for AuthPolicy { } } } + +// 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: Url, + /// The root endpoint where authentication is applied. + /// For PEP 503 endpoints, this excludes `/simple`. + pub root_url: Url, + pub auth_policy: AuthPolicy, +} + #[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct UrlAuthPolicies(FxHashMap); +pub struct Indexes(FxHashSet); -impl UrlAuthPolicies { +impl Indexes { pub fn new() -> Self { - Self(FxHashMap::default()) + Self(FxHashSet::default()) } - /// Create a new [`UrlAuthPolicies`] from a list of URL and [`AuthPolicy`] - /// tuples. - pub fn from_tuples(tuples: impl IntoIterator) -> Self { - let mut auth_policies = Self::new(); - for (url, auth_policy) in tuples { - auth_policies.add_policy(url, auth_policy); + /// Create a new [`AuthIndexUrls`] from an iterator of [`AuthIndexUrl`]s. + pub fn from_indexes(urls: impl IntoIterator) -> Self { + let mut index_urls = Self::new(); + for url in urls { + index_urls.0.insert(url); } - auth_policies + index_urls } - /// An [`AuthPolicy`] for a URL. - pub fn add_policy(&mut self, url: Url, auth_policy: AuthPolicy) { - self.0.insert(url, auth_policy); + /// Get the index URL prefix for a URL if one exists. + pub fn index_url_for(&self, url: &Url) -> Option<&Url> { + // TODO(john): There are probably not many URLs to iterate through, + // but we could use a trie instead of a HashSet here for more + // efficient search. + self.0 + .iter() + .find(|index| is_url_prefix(&index.root_url, url)) + .map(|index| &index.url) } /// Get the [`AuthPolicy`] for a URL. @@ -75,11 +93,22 @@ impl UrlAuthPolicies { // TODO(john): There are probably not many URLs to iterate through, // but we could use a trie instead of a HashMap here for more // efficient search. - for (auth_url, auth_policy) in &self.0 { - if url.as_str().starts_with(auth_url.as_str()) { - return *auth_policy; + for index in &self.0 { + if is_url_prefix(&index.root_url, url) { + return index.auth_policy; } } AuthPolicy::Auto } } + +fn is_url_prefix(base: &Url, url: &Url) -> bool { + if base.scheme() != url.scheme() + || base.host_str() != url.host_str() + || base.port_or_known_default() != url.port_or_known_default() + { + return false; + } + + url.path().starts_with(base.path()) +} diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 1c090e73f..6aa96a245 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -5,16 +5,16 @@ use url::Url; use cache::CredentialsCache; pub use credentials::Credentials; +pub use index::{AuthPolicy, Index, Indexes}; pub use keyring::KeyringProvider; pub use middleware::AuthMiddleware; -pub use policy::{AuthPolicy, UrlAuthPolicies}; use realm::Realm; mod cache; mod credentials; +mod index; mod keyring; mod middleware; -mod policy; mod realm; // TODO(zanieb): Consider passing a cache explicitly throughout diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 308f7b980..544351b58 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -5,7 +5,7 @@ use url::Url; use crate::{ credentials::{Credentials, Username}, - policy::{AuthPolicy, UrlAuthPolicies}, + index::{AuthPolicy, Indexes}, realm::Realm, CredentialsCache, KeyringProvider, CREDENTIALS_CACHE, }; @@ -58,7 +58,7 @@ pub struct AuthMiddleware { keyring: Option, cache: Option, /// Auth policies for specific URLs. - url_auth_policies: UrlAuthPolicies, + indexes: Indexes, /// Set all endpoints as needing authentication. We never try to send an /// unauthenticated request, avoiding cloning an uncloneable request. only_authenticated: bool, @@ -70,7 +70,7 @@ impl AuthMiddleware { netrc: NetrcMode::default(), keyring: None, cache: None, - url_auth_policies: UrlAuthPolicies::new(), + indexes: Indexes::new(), only_authenticated: false, } } @@ -104,8 +104,8 @@ impl AuthMiddleware { /// Configure the [`AuthPolicy`]s to use for URLs. #[must_use] - pub fn with_url_auth_policies(mut self, auth_policies: UrlAuthPolicies) -> Self { - self.url_auth_policies = auth_policies; + pub fn with_indexes(mut self, indexes: Indexes) -> Self { + self.indexes = indexes; self } @@ -148,7 +148,7 @@ impl Middleware for AuthMiddleware { /// We'll avoid making a request we expect to fail and look for a password. /// The discovered credentials must have the requested username to be used. /// - /// - Check the cache (realm key) for a password + /// - Check the cache (index URL or realm key) for a password /// - Check the netrc for a password /// - Check the keyring for a password /// - Perform the request @@ -162,10 +162,10 @@ impl Middleware for AuthMiddleware { /// server tells us authorization is needed. This pattern avoids attaching credentials to /// requests that do not need them, which can cause some servers to deny the request. /// - /// - Check the cache (url key) + /// - Check the cache (URL key) /// - Perform the request /// - On 401, 403, or 404 check for authentication if there was a cache miss - /// - Check the cache (realm key) for the username and password + /// - Check the cache (index URL or realm key) for the username and password /// - Check the netrc for a username and password /// - Perform the request again if found /// - Add the username and password to the cache if successful @@ -181,7 +181,8 @@ impl Middleware for AuthMiddleware { // In the middleware, existing credentials are already moved from the URL // to the headers so for display purposes we restore some information let url = tracing_url(&request, request_credentials.as_ref()); - let auth_policy = self.url_auth_policies.policy_for(request.url()); + let maybe_index_url = self.indexes.index_url_for(request.url()); + let auth_policy = self.indexes.policy_for(request.url()); trace!("Handling request for {url} with authentication policy {auth_policy}"); let credentials: Option> = if matches!(auth_policy, AuthPolicy::Never) { @@ -195,6 +196,7 @@ impl Middleware for AuthMiddleware { extensions, next, &url, + maybe_index_url, auth_policy, ) .await; @@ -272,17 +274,20 @@ impl Middleware for AuthMiddleware { (request, None) }; - // Check if there are credentials in the realm-level cache - let credentials = self - .cache() - .get_realm( - Realm::from(retry_request.url()), - credentials - .as_ref() - .map(|credentials| credentials.to_username()) - .unwrap_or(Username::none()), - ) - .or(credentials); + let username = credentials + .as_ref() + .map(|credentials| credentials.to_username()) + .unwrap_or(Username::none()); + let credentials = if let Some(index_url) = maybe_index_url { + self.cache().get_url(index_url, &username) + } else { + // Since there is no known index for this URL, check if there are credentials in + // the realm-level cache. + self.cache() + .get_realm(Realm::from(retry_request.url()), username) + } + .or(credentials); + if let Some(credentials) = credentials.as_ref() { if credentials.password().is_some() { trace!("Retrying request for {url} with credentials from cache {credentials:?}"); @@ -296,7 +301,12 @@ impl Middleware for AuthMiddleware { // Then, fetch from external services. // Here, we use the username from the cache if present. if let Some(credentials) = self - .fetch_credentials(credentials.as_deref(), retry_request.url(), auth_policy) + .fetch_credentials( + credentials.as_deref(), + retry_request.url(), + maybe_index_url, + auth_policy, + ) .await { retry_request = credentials.authenticate(retry_request); @@ -374,6 +384,7 @@ impl AuthMiddleware { extensions: &mut Extensions, next: Next<'_>, url: &str, + maybe_index_url: Option<&Url>, auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let credentials = Arc::new(credentials); @@ -387,15 +398,27 @@ impl AuthMiddleware { } trace!("Request for {url} is missing a password, looking for credentials"); - // There's just a username, try to find a password - let credentials = if let Some(credentials) = self - .cache() - .get_realm(Realm::from(request.url()), credentials.to_username()) - { + + // There's just a username, try to find a password. + // If we have an index URL, check the cache for that URL. Otherwise, + // check for the realm. + let maybe_cached_credentials = if let Some(index_url) = maybe_index_url { + self.cache() + .get_url(index_url, credentials.as_username().as_ref()) + } else { + self.cache() + .get_realm(Realm::from(request.url()), credentials.to_username()) + }; + if let Some(credentials) = maybe_cached_credentials { request = credentials.authenticate(request); // Do not insert already-cached credentials - None - } else if let Some(credentials) = self + let credentials = None; + return self + .complete_request(credentials, request, extensions, next, auth_policy) + .await; + } + + let credentials = if let Some(credentials) = self .cache() .get_url(request.url(), credentials.as_username().as_ref()) { @@ -403,11 +426,21 @@ impl AuthMiddleware { // Do not insert already-cached credentials None } else if let Some(credentials) = self - .fetch_credentials(Some(&credentials), request.url(), auth_policy) + .fetch_credentials( + Some(&credentials), + request.url(), + maybe_index_url, + auth_policy, + ) .await { request = credentials.authenticate(request); Some(credentials) + } else if maybe_index_url.is_some() { + // If this is a known index, we fall back to checking for the realm. + self.cache() + .get_realm(Realm::from(request.url()), credentials.to_username()) + .or(Some(credentials)) } else { // If we don't find a password, we'll still attempt the request with the existing credentials Some(credentials) @@ -425,18 +458,20 @@ impl AuthMiddleware { &self, credentials: Option<&Credentials>, url: &Url, + maybe_index_url: Option<&Url>, auth_policy: AuthPolicy, ) -> Option> { - // Fetches can be expensive, so we will only run them _once_ per realm and username combination - // All other requests for the same realm will wait until the first one completes - let key = ( - Realm::from(url), - Username::from( - credentials - .map(|credentials| credentials.username().unwrap_or_default().to_string()), - ), + let username = Username::from( + credentials.map(|credentials| credentials.username().unwrap_or_default().to_string()), ); + // Fetches can be expensive, so we will only run them _once_ per realm or index URL and username combination + // All other requests for the same realm or index URL will wait until the first one completes + let key = if let Some(index_url) = maybe_index_url { + (index_url.to_string(), username) + } else { + (Realm::from(url).to_string(), username) + }; if !self.cache().fetches.register(key.clone()) { let credentials = self .cache() @@ -446,9 +481,12 @@ impl AuthMiddleware { .expect("The key must exist after register is called"); if credentials.is_some() { - trace!("Using credentials from previous fetch for {url}"); + trace!("Using credentials from previous fetch for {}", key.0); } else { - trace!("Skipping fetch of credentials for {url}, previous attempt failed"); + trace!( + "Skipping fetch of credentials for {}, previous attempt failed", + key.0 + ); } return credentials; @@ -468,22 +506,35 @@ impl AuthMiddleware { debug!("Found credentials in netrc file for {url}"); Some(credentials) - // N.B. The keyring provider performs lookups for the exact URL then falls back to the host, - // but we cache the result per realm so if a keyring implementation returns different - // credentials for different URLs in the same realm we will use the wrong credentials. + // N.B. The keyring provider performs lookups for the exact URL then falls back to the host. + // But, in the absence of an index URL, we cache the result per realm. So in that case, + // if a keyring implementation returns different credentials for different URLs in the + // same realm we will use the wrong credentials. } else if let Some(credentials) = match self.keyring { Some(ref keyring) => { // The subprocess keyring provider is _slow_ so we do not perform fetches for all // URLs; instead, we fetch if there's a username or if the user has requested to // always authenticate. if let Some(username) = credentials.and_then(|credentials| credentials.username()) { - debug!("Checking keyring for credentials for {username}@{url}"); - keyring.fetch(url, Some(username)).await + if let Some(index_url) = maybe_index_url { + debug!("Checking keyring for credentials for index URL {}@{}", username, index_url); + keyring.fetch(index_url, Some(username)).await + } else { + debug!("Checking keyring for credentials for full URL {}@{}", username, *url); + keyring.fetch(url, Some(username)).await + } } else if matches!(auth_policy, AuthPolicy::Always) { - debug!( - "Checking keyring for credentials for {url} without username due to `authenticate = always`" - ); - keyring.fetch(url, None).await + if let Some(index_url) = maybe_index_url { + debug!( + "Checking keyring for credentials for index URL {index_url} without username due to `authenticate = always`" + ); + keyring.fetch(index_url, None).await + } else { + debug!( + "Checking keyring for credentials for full URL {url} without username due to `authenticate = always`" + ); + keyring.fetch(url, None).await + } } else { debug!("Skipping keyring fetch for {url} without username; use `authenticate = always` to force"); None @@ -499,7 +550,7 @@ impl AuthMiddleware { .map(Arc::new); // Register the fetch for this key - self.cache().fetches.done(key.clone(), credentials.clone()); + self.cache().fetches.done(key, credentials.clone()); credentials } @@ -539,6 +590,7 @@ mod tests { use wiremock::{Mock, MockServer, ResponseTemplate}; use crate::credentials::Password; + use crate::Index; use super::*; @@ -999,7 +1051,7 @@ mod tests { let server = start_test_server(username, password).await; let base_url = Url::parse(&server.uri())?; - let auth_policies = auth_policies_for(&base_url, AuthPolicy::Always); + let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() @@ -1013,7 +1065,7 @@ mod tests { username, password, )]))) - .with_url_auth_policies(auth_policies), + .with_indexes(indexes), ) .build(); @@ -1656,13 +1708,213 @@ mod tests { Ok(()) } - fn auth_policies_for(url: &Url, policy: AuthPolicy) -> UrlAuthPolicies { + /// Demonstrates that when an index URL is provided, we avoid "incorrect" behavior + /// where multiple URLs with the same username and realm share the same realm-level + /// credentials cache entry. + #[test(tokio::test)] + async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm( + ) -> Result<(), Error> { + let username = "user"; + let password_1 = "password1"; + let password_2 = "password2"; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_1.*")) + .and(basic_auth(username, password_1)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_2.*")) + .and(basic_auth(username, password_2)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + let base_url = Url::parse(&server.uri())?; + let base_url_1 = base_url.join("prefix_1")?; + let base_url_2 = base_url.join("prefix_2")?; + let indexes = Indexes::from_indexes(vec![ + Index { + url: base_url_1.clone(), + root_url: base_url_1.clone(), + auth_policy: AuthPolicy::Auto, + }, + Index { + url: base_url_2.clone(), + root_url: base_url_2.clone(), + auth_policy: AuthPolicy::Auto, + }, + ]); + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([ + (base_url_1.clone(), username, password_1), + (base_url_2.clone(), username, password_2), + ]))) + .with_indexes(indexes), + ) + .build(); + + // Both servers do not work without a username + assert_eq!( + client.get(base_url_1.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + + let mut url_1 = base_url_1.clone(); + url_1.set_username(username).unwrap(); + assert_eq!( + client.get(url_1.clone()).send().await?.status(), + 200, + "The first request with a username will succeed" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Credentials should not be re-used for the second prefix" + ); + assert_eq!( + client + .get(base_url.join("prefix_1/foo")?) + .send() + .await? + .status(), + 200, + "Subsequent requests can be to different paths in the same prefix" + ); + + let mut url_2 = base_url_2.clone(); + url_2.set_username(username).unwrap(); + assert_eq!( + client.get(url_2.clone()).send().await?.status(), + 200, + "A request with the same username and realm for a URL will use index-specific password" + ); + assert_eq!( + client + .get(base_url.join("prefix_2/foo")?) + .send() + .await? + .status(), + 200, + "Requests to other paths with that prefix will also succeed" + ); + + Ok(()) + } + + /// Demonstrates that when an index' credentials are cached for its realm, we + /// find those credentials if they're not present in the keyring. + #[test(tokio::test)] + async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm( + ) -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_1.*")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + let base_url = Url::parse(&server.uri())?; + let index_url = base_url.join("prefix_1")?; + let indexes = Indexes::from_indexes(vec![Index { + url: index_url.clone(), + root_url: index_url.clone(), + auth_policy: AuthPolicy::Auto, + }]); + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([( + base_url.clone(), + username, + password, + )]))) + .with_indexes(indexes), + ) + .build(); + + // Index server does not work without a username + assert_eq!( + client.get(index_url.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + + // Send a request that will cache realm credentials. + let mut realm_url = base_url.clone(); + realm_url.set_username(username).unwrap(); + assert_eq!( + client.get(realm_url.clone()).send().await?.status(), + 200, + "The first realm request with a username will succeed" + ); + + let mut url = index_url.clone(); + url.set_username(username).unwrap(); + assert_eq!( + client.get(url.clone()).send().await?.status(), + 200, + "A request with the same username and realm for a URL will use the realm if there is no index-specific password" + ); + assert_eq!( + client + .get(base_url.join("prefix_1/foo")?) + .send() + .await? + .status(), + 200, + "Requests to other paths with that prefix will also succeed" + ); + + Ok(()) + } + + fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes { let mut url = url.clone(); - let mut policies = UrlAuthPolicies::new(); url.set_password(None).ok(); url.set_username("").ok(); - policies.add_policy(url, policy); - policies + Indexes::from_indexes(vec![Index { + url: url.clone(), + root_url: url.clone(), + auth_policy: policy, + }]) } /// With the "always" auth policy, requests should succeed on @@ -1676,12 +1928,12 @@ mod tests { let base_url = Url::parse(&server.uri())?; - let auth_policies = auth_policies_for(&base_url, AuthPolicy::Always); + let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) - .with_url_auth_policies(auth_policies), + .with_indexes(indexes), ) .build(); @@ -1743,12 +1995,12 @@ mod tests { let base_url = Url::parse(&server.uri())?; - let auth_policies = auth_policies_for(&base_url, AuthPolicy::Always); + let indexes = indexes_for(&base_url, AuthPolicy::Always); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) - .with_url_auth_policies(auth_policies), + .with_indexes(indexes), ) .build(); @@ -1783,12 +2035,12 @@ mod tests { .mount(&server) .await; - let auth_policies = auth_policies_for(&base_url, AuthPolicy::Never); + let indexes = indexes_for(&base_url, AuthPolicy::Never); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) - .with_url_auth_policies(auth_policies), + .with_indexes(indexes), ) .build(); @@ -1828,12 +2080,12 @@ mod tests { let base_url = Url::parse(&server.uri())?; - let auth_policies = auth_policies_for(&base_url, AuthPolicy::Never); + let indexes = indexes_for(&base_url, AuthPolicy::Never); let client = test_client_builder() .with( AuthMiddleware::new() .with_cache(CredentialsCache::new()) - .with_url_auth_policies(auth_policies), + .with_indexes(indexes), ) .build(); diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 0297c1721..dc8a5dd3f 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -19,7 +19,7 @@ use tracing::{debug, trace}; use url::ParseError; use url::Url; -use uv_auth::{AuthMiddleware, UrlAuthPolicies}; +use uv_auth::{AuthMiddleware, Indexes}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; use uv_pep508::MarkerEnvironment; @@ -59,7 +59,7 @@ pub struct BaseClientBuilder<'a> { markers: Option<&'a MarkerEnvironment>, platform: Option<&'a Platform>, auth_integration: AuthIntegration, - url_auth_policies: Option, + indexes: Indexes, default_timeout: Duration, extra_middleware: Option, proxies: Vec, @@ -112,7 +112,7 @@ impl BaseClientBuilder<'_> { markers: None, platform: None, auth_integration: AuthIntegration::default(), - url_auth_policies: None, + indexes: Indexes::new(), default_timeout: Duration::from_secs(30), extra_middleware: None, proxies: vec![], @@ -171,8 +171,8 @@ impl<'a> BaseClientBuilder<'a> { } #[must_use] - pub fn url_auth_policies(mut self, auth_policies: UrlAuthPolicies) -> Self { - self.url_auth_policies = Some(auth_policies); + pub fn indexes(mut self, indexes: Indexes) -> Self { + self.indexes = indexes; self } @@ -386,20 +386,18 @@ impl<'a> BaseClientBuilder<'a> { // Initialize the authentication middleware to set headers. match self.auth_integration { AuthIntegration::Default => { - let mut auth_middleware = - AuthMiddleware::new().with_keyring(self.keyring.to_provider()); - if let Some(url_auth_policies) = &self.url_auth_policies { - auth_middleware = - auth_middleware.with_url_auth_policies(url_auth_policies.clone()); - } + let auth_middleware = AuthMiddleware::new() + .with_indexes(self.indexes.clone()) + .with_keyring(self.keyring.to_provider()); client = client.with(auth_middleware); } AuthIntegration::OnlyAuthenticated => { - client = client.with( - AuthMiddleware::new() - .with_keyring(self.keyring.to_provider()) - .with_only_authenticated(true), - ); + let auth_middleware = AuthMiddleware::new() + .with_indexes(self.indexes.clone()) + .with_keyring(self.keyring.to_provider()) + .with_only_authenticated(true); + + client = client.with(auth_middleware); } AuthIntegration::NoAuthMiddleware => { // The downstream code uses custom auth logic. diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 5aa9b230e..45cafc692 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -15,14 +15,14 @@ use tokio::sync::{Mutex, Semaphore}; use tracing::{info_span, instrument, trace, warn, Instrument}; use url::Url; -use uv_auth::UrlAuthPolicies; +use uv_auth::Indexes; use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache}; use uv_configuration::KeyringProviderType; use uv_configuration::{IndexStrategy, TrustedHost}; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, FileLocation, IndexCapabilities, IndexFormat, IndexMetadataRef, IndexUrl, - IndexUrls, Name, + BuiltDist, File, FileLocation, IndexCapabilities, IndexFormat, IndexLocations, + IndexMetadataRef, IndexUrl, IndexUrls, Name, }; use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream}; use uv_normalize::PackageName; @@ -68,8 +68,11 @@ impl RegistryClientBuilder<'_> { impl<'a> RegistryClientBuilder<'a> { #[must_use] - pub fn index_urls(mut self, index_urls: IndexUrls) -> Self { - self.index_urls = index_urls; + pub fn index_locations(mut self, index_locations: &IndexLocations) -> Self { + self.index_urls = index_locations.index_urls(); + self.base_client_builder = self + .base_client_builder + .indexes(Indexes::from(index_locations)); self } @@ -117,14 +120,6 @@ impl<'a> RegistryClientBuilder<'a> { self } - #[must_use] - pub fn url_auth_policies(mut self, url_auth_policies: UrlAuthPolicies) -> Self { - self.base_client_builder = self - .base_client_builder - .url_auth_policies(url_auth_policies); - self - } - #[must_use] pub fn cache(mut self, cache: Cache) -> Self { self.cache = cache; diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index ef3c324fb..fdb4ea1e5 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -10,7 +10,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use thiserror::Error; use url::{ParseError, Url}; -use uv_auth::UrlAuthPolicies; use uv_pep508::{split_scheme, Scheme, VerbatimUrl, VerbatimUrlError}; use crate::{Index, Verbatim}; @@ -411,16 +410,20 @@ impl<'a> IndexLocations { } } -impl From<&IndexLocations> for UrlAuthPolicies { - fn from(index_locations: &IndexLocations) -> UrlAuthPolicies { - UrlAuthPolicies::from_tuples(index_locations.allowed_indexes().into_iter().map(|index| { - let mut url = index - .url() - .root() - .unwrap_or_else(|| index.url().url().clone()); +impl From<&IndexLocations> for uv_auth::Indexes { + fn from(index_locations: &IndexLocations) -> uv_auth::Indexes { + uv_auth::Indexes::from_indexes(index_locations.allowed_indexes().into_iter().map(|index| { + let mut url = index.url().url().clone(); url.set_username("").ok(); url.set_password(None).ok(); - (url, index.authenticate) + let mut root_url = index.url().root().unwrap_or_else(|| url.clone()); + root_url.set_username("").ok(); + root_url.set_password(None).ok(); + uv_auth::Index { + url, + root_url, + auth_policy: index.authenticate, + } })) } } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 033666b2b..55580ce36 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -10,7 +10,6 @@ use anyhow::{Context, Result}; use owo_colors::OwoColorize; use thiserror::Error; use tracing::instrument; -use uv_auth::UrlAuthPolicies; use crate::commands::pip::operations; use crate::commands::project::{find_requires_python, ProjectError}; @@ -528,8 +527,7 @@ async fn build_package( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(index_strategy) .keyring(keyring_provider) .markers(interpreter.markers()) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index b14e2e89c..06106ba5d 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -10,7 +10,6 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashSet; use tracing::debug; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -392,9 +391,8 @@ pub(crate) async fn pip_compile( // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? .cache(cache.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(&index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&index_locations)) .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index ca238d01b..0b7157ccf 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -7,7 +7,6 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, enabled, Level}; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -359,9 +358,8 @@ pub(crate) async fn pip_install( // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? .cache(cache.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(&index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&index_locations)) .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 3d458e1d7..a26954c6f 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -11,7 +11,6 @@ use serde::Serialize; use tokio::sync::Semaphore; use unicode_width::UnicodeWidthStr; -use uv_auth::UrlAuthPolicies; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::ListFormat; @@ -89,9 +88,8 @@ pub(crate) async fn pip_list( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(&index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&index_locations)) .keyring(keyring_provider) .markers(environment.interpreter().markers()) .platform(environment.interpreter().platform()) diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 163492886..ad4f74b1f 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -6,7 +6,6 @@ use anyhow::Result; use owo_colors::OwoColorize; use tracing::debug; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -291,9 +290,8 @@ pub(crate) async fn pip_sync( // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? .cache(cache.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(&index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&index_locations)) .torch_backend(torch_backend.clone()) .markers(interpreter.markers()) .platform(interpreter.platform()) diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index 1776ea4b9..fc1252e39 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -10,7 +10,6 @@ use petgraph::Direction; use rustc_hash::{FxHashMap, FxHashSet}; use tokio::sync::Semaphore; -use uv_auth::UrlAuthPolicies; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::RegistryClientBuilder; @@ -90,9 +89,8 @@ pub(crate) async fn pip_tree( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(&index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&index_locations)) .keyring(keyring_provider) .markers(environment.interpreter().markers()) .platform(environment.interpreter().platform()) diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 7c7b9304a..111972bb2 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -13,7 +13,6 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; use url::Url; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -323,9 +322,8 @@ pub(crate) async fn add( // Initialize the registry client. let client = RegistryClientBuilder::try_from(client_builder)? - .index_urls(settings.resolver.index_locations.index_urls()) + .index_locations(&settings.resolver.index_locations) .index_strategy(settings.resolver.index_strategy) - .url_auth_policies(UrlAuthPolicies::from(&settings.resolver.index_locations)) .markers(target.interpreter().markers()) .platform(target.interpreter().platform()) .build(); diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index d311b5398..86a12112b 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -9,7 +9,6 @@ use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -593,8 +592,7 @@ async fn do_lock( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(*index_strategy) .keyring(*keyring_provider) .markers(interpreter.markers()) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 1677754a2..53e51f808 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -8,7 +8,6 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, trace, warn}; -use uv_auth::UrlAuthPolicies; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::cache_digest; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -1564,8 +1563,7 @@ pub(crate) async fn resolve_names( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(*index_strategy) .keyring(*keyring_provider) .markers(interpreter.markers()) @@ -1717,8 +1715,7 @@ pub(crate) async fn resolve_environment( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(*index_strategy) .keyring(*keyring_provider) .markers(interpreter.markers()) @@ -1891,8 +1888,7 @@ pub(crate) async fn sync_environment( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(index_strategy) .keyring(keyring_provider) .markers(interpreter.markers()) @@ -2104,8 +2100,7 @@ pub(crate) async fn update_environment( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(*index_strategy) .keyring(*keyring_provider) .markers(interpreter.markers()) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index d95dfc016..6ce76cf53 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -7,7 +7,6 @@ use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -683,8 +682,7 @@ pub(super) async fn do_sync( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(index_strategy) .keyring(keyring_provider) .markers(venv.interpreter().markers()) diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index f88b4a381..86ad6d61b 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -4,7 +4,6 @@ use anstream::print; use anyhow::{Error, Result}; use futures::StreamExt; use tokio::sync::Semaphore; -use uv_auth::UrlAuthPolicies; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::RegistryClientBuilder; @@ -212,7 +211,7 @@ pub(crate) async fn tree( .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) + .index_locations(index_locations) .keyring(*keyring_provider) .build(); let download_concurrency = Semaphore::new(concurrency.downloads); diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index f86de83a9..9190e1a90 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -89,17 +89,16 @@ pub(crate) async fn publish( // Initialize the registry client. let check_url_client = if let Some(index_url) = &check_url { - let index_urls = IndexLocations::new( + let index_locations = IndexLocations::new( vec![Index::from_index_url(index_url.clone())], Vec::new(), false, - ) - .index_urls(); + ); let registry_client_builder = RegistryClientBuilder::new(cache.clone()) .native_tls(network_settings.native_tls) .connectivity(network_settings.connectivity) .allow_insecure_host(network_settings.allow_insecure_host.clone()) - .index_urls(index_urls) + .index_locations(&index_locations) .keyring(keyring_provider); Some(CheckUrlClient { index_url: index_url.clone(), diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 2580d99e1..8d2087c57 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -10,7 +10,6 @@ use miette::{Diagnostic, IntoDiagnostic}; use owo_colors::OwoColorize; use thiserror::Error; -use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -296,9 +295,8 @@ async fn venv_impl( let client = RegistryClientBuilder::try_from(client_builder) .into_diagnostic()? .cache(cache.clone()) - .index_urls(index_locations.index_urls()) + .index_locations(index_locations) .index_strategy(index_strategy) - .url_auth_policies(UrlAuthPolicies::from(index_locations)) .keyring(keyring_provider) .allow_insecure_host(network_settings.allow_insecure_host.clone()) .markers(interpreter.markers()) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 1f0f477fa..05842211a 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -10633,6 +10633,124 @@ fn add_unsupported_git_scheme() { "###); } +#[test] +fn add_index_url_in_keyring() -> Result<()> { + let keyring_context = TestContext::new("3.12"); + + // Install our keyring plugin + keyring_context + .pip_install() + .arg( + keyring_context + .workspace_root + .join("scripts") + .join("packages") + .join("keyring_test_plugin"), + ) + .assert() + .success(); + + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.11, <4" + dependencies = [] + + [tool.uv] + keyring-provider = "subprocess" + + [[tool.uv.index]] + name = "proxy" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true + "# + })?; + + uv_snapshot!(context.add().arg("anyio") + .env(EnvVars::index_username("PROXY"), "public") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"https://pypi-proxy.fly.dev/basic-auth/simple": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + " + ); + + context.assert_command("import anyio").success(); + Ok(()) +} + +#[test] +fn add_full_url_in_keyring() -> Result<()> { + let keyring_context = TestContext::new("3.12"); + + // Install our keyring plugin + keyring_context + .pip_install() + .arg( + keyring_context + .workspace_root + .join("scripts") + .join("packages") + .join("keyring_test_plugin"), + ) + .assert() + .success(); + + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.11, <4" + dependencies = [] + + [tool.uv] + keyring-provider = "subprocess" + + [[tool.uv.index]] + name = "proxy" + url = "https://pypi-proxy.fly.dev/basic-auth/simple" + default = true + "# + })?; + + uv_snapshot!(context.add().arg("anyio") + .env(EnvVars::index_username("PROXY"), "public") + .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"https://pypi-proxy.fly.dev/basic-auth/simple/anyio": {"public": "heron"}}"#) + .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev + × No solution found when resolving dependencies: + ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. + + hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). + help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + " + ); + Ok(()) +} + /// In authentication "always", the normal authentication flow should still work. #[test] fn add_auth_policy_always_with_credentials() -> Result<()> { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 5c7a1202b..7a1f1e32a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -18803,16 +18803,16 @@ fn lock_keyring_credentials() -> Result<()> { uv_snapshot!(context.filters(), context.lock() .env(EnvVars::index_username("PROXY"), "public") .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) - .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&keyring_context.venv)), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ - Request for public@pypi-proxy.fly.dev + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev Resolved 2 packages in [TIME] - "###); + "); let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); @@ -18914,8 +18914,8 @@ fn lock_keyring_explicit_always() -> Result<()> { ----- stdout ----- ----- stderr ----- - Request for https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ - Request for pypi-proxy.fly.dev + Keyring request for https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for pypi-proxy.fly.dev × 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. @@ -18931,8 +18931,8 @@ fn lock_keyring_explicit_always() -> Result<()> { ----- stdout ----- ----- stderr ----- - Request for https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ - Request for pypi-proxy.fly.dev + Keyring request for https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for pypi-proxy.fly.dev Resolved 2 packages in [TIME] "); @@ -18997,8 +18997,8 @@ fn lock_keyring_credentials_always_authenticate_fetches_username() -> Result<()> ----- stdout ----- ----- stderr ----- - Request for https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ - Request for pypi-proxy.fly.dev + Keyring request for https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for pypi-proxy.fly.dev Resolved 2 packages in [TIME] "); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 1efee1286..390c9c9b4 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -5461,21 +5461,21 @@ fn install_package_basic_auth_from_keyring() { .arg("subprocess") .arg("--strict") .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#) - .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/ - Request for public@pypi-proxy.fly.dev + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev Resolved 3 packages in [TIME] Prepared 3 packages in [TIME] Installed 3 packages in [TIME] + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "### + " ); context.assert_command("import anyio").success(); @@ -5508,19 +5508,19 @@ fn install_package_basic_auth_from_keyring_wrong_password() { .arg("subprocess") .arg("--strict") .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"public": "foobar"}}"#) - .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/ - Request for public@pypi-proxy.fly.dev + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev × No solution found when resolving dependencies: ╰─▶ Because anyio was not found in the package registry and you require anyio, we can conclude that your requirements are unsatisfiable. hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). - "### + " ); } @@ -5551,19 +5551,19 @@ fn install_package_basic_auth_from_keyring_wrong_username() { .arg("subprocess") .arg("--strict") .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"pypi-proxy.fly.dev": {"other": "heron"}}"#) - .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - Request for public@https://pypi-proxy.fly.dev/basic-auth/simple/anyio/ - Request for public@pypi-proxy.fly.dev + Keyring request for public@https://pypi-proxy.fly.dev/basic-auth/simple + Keyring request for public@pypi-proxy.fly.dev × No solution found when resolving dependencies: ╰─▶ Because anyio was not found in the package registry and you require anyio, we can conclude that your requirements are unsatisfiable. hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). - "### + " ); } diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs index ae33eea84..a45ccb801 100644 --- a/crates/uv/tests/it/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -276,22 +276,22 @@ fn check_keyring_behaviours() { .arg("--publish-url") .arg("https://test.pypi.org/legacy/?ok") .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") - .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Publishing 1 file to https://test.pypi.org/legacy/?ok - Request for dummy@https://test.pypi.org/legacy/?ok - Request for dummy@test.pypi.org + Keyring request for dummy@https://test.pypi.org/legacy/?ok + Keyring request for dummy@test.pypi.org warning: Keyring has no password for URL `https://test.pypi.org/legacy/?ok` and username `dummy` Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) - Request for dummy@https://test.pypi.org/legacy/?ok - Request for dummy@test.pypi.org + Keyring request for dummy@https://test.pypi.org/legacy/?ok + Keyring request for dummy@test.pypi.org error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/?ok Caused by: Upload failed with status code 403 Forbidden. Server says: 403 Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See https://test.pypi.org/help/#apitoken and https://test.pypi.org/help/#trusted-publishers - "### + " ); // Ok: There is a keyring entry for the user dummy. @@ -305,18 +305,18 @@ fn check_keyring_behaviours() { .arg("https://test.pypi.org/legacy/?ok") .arg("../../scripts/links/ok-1.0.0-py3-none-any.whl") .env(EnvVars::KEYRING_TEST_CREDENTIALS, r#"{"https://test.pypi.org/legacy/?ok": {"dummy": "dummy"}}"#) - .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###" + .env(EnvVars::PATH, venv_bin_path(&context.venv)), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- Publishing 1 file to https://test.pypi.org/legacy/?ok - Request for dummy@https://test.pypi.org/legacy/?ok + Keyring request for dummy@https://test.pypi.org/legacy/?ok Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/?ok Caused by: Upload failed with status code 403 Forbidden. Server says: 403 Username/Password authentication is no longer supported. Migrate to API Tokens or Trusted Publishers instead. See https://test.pypi.org/help/#apitoken and https://test.pypi.org/help/#trusted-publishers - "### + " ); } diff --git a/docs/configuration/authentication.md b/docs/configuration/authentication.md index 2425fc262..a05a637ba 100644 --- a/docs/configuration/authentication.md +++ b/docs/configuration/authentication.md @@ -38,9 +38,9 @@ Authentication can come from the following sources, in order of precedence: - A [`.netrc`](https://everything.curl.dev/usingcurl/netrc) configuration file - A [keyring](https://github.com/jaraco/keyring) provider (requires opt-in) -If authentication is found for a single net location (scheme, host, and port), it will be cached for -the duration of the command and used for other queries to that net location. Authentication is not -cached across invocations of uv. +If authentication is found for a single index URL or net location (scheme, host, and port), it will +be cached for the duration of the command and used for other queries to that index or net location. +Authentication is not cached across invocations of uv. `.netrc` authentication is enabled by default, and will respect the `NETRC` environment variable if defined, falling back to `~/.netrc` if not. diff --git a/scripts/packages/keyring_test_plugin/keyrings/test_keyring.py b/scripts/packages/keyring_test_plugin/keyrings/test_keyring.py index f06950602..59f7b0d6d 100644 --- a/scripts/packages/keyring_test_plugin/keyrings/test_keyring.py +++ b/scripts/packages/keyring_test_plugin/keyrings/test_keyring.py @@ -9,7 +9,7 @@ class KeyringTest(backend.KeyringBackend): priority = 9 def get_password(self, service, username): - print(f"Request for {username}@{service}", file=sys.stderr) + print(f"Keyring request for {username}@{service}", file=sys.stderr) entries = json.loads(os.environ.get("KEYRING_TEST_CREDENTIALS", "{}")) return entries.get(service, {}).get(username) @@ -20,7 +20,7 @@ class KeyringTest(backend.KeyringBackend): raise NotImplementedError() def get_credential(self, service, username): - print(f"Request for {service}", file=sys.stderr) + print(f"Keyring request for {service}", file=sys.stderr) entries = json.loads(os.environ.get("KEYRING_TEST_CREDENTIALS", "{}")) service_entries = entries.get(service, {}) if not service_entries: