Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions crates/uv-auth/src/keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ static UV_SERVICE_PREFIX: &str = "uv-credentials:";
///
/// See pip's implementation for reference
/// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L102>
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct KeyringProvider {
backend: KeyringProviderBackend,
}

#[derive(Debug)]
#[derive(Clone, Debug)]
pub(crate) enum KeyringProviderBackend {
/// Use system keyring integration to fetch credentials.
Native,
Expand All @@ -50,6 +50,11 @@ impl KeyringProvider {
}
}

/// Whether the backend is [`KeyringProviderBackend::Native`].
pub fn is_native(&self) -> bool {
matches!(self.backend, KeyringProviderBackend::Native)
}

/// Store credentials for the given [`Url`] to the keyring if the
/// keyring provider backend is `Native`.
#[instrument(skip_all, fields(url = % url.to_string(), username))]
Expand Down Expand Up @@ -109,7 +114,22 @@ impl KeyringProvider {
}
}

/// Fetch credentials for the given [`Url`] from the keyring.
/// Fetch credentials for the given [`DisplaySafeUrl`] from the keyring if the backend is
/// [`KeyringProviderBackend::Native`].
#[instrument(skip_all, fields(url = % url.to_string(), username))]
pub async fn fetch_if_native(
&self,
url: &DisplaySafeUrl,
username: Option<&str>,
) -> Option<Credentials> {
if self.is_native() {
self.fetch(url, username).await
} else {
None
}
}

/// Fetch credentials for the given [`DisplaySafeUrl`] from the keyring.
///
/// Returns [`None`] if no password was found for the username or if any errors
/// are encountered in the keyring backend.
Expand Down Expand Up @@ -160,6 +180,10 @@ impl KeyringProvider {
};
}

if credentials.is_some() {
debug!("Found credentials in keyring for {url}");
}

credentials.map(|(username, password)| Credentials::basic(Some(username), Some(password)))
}

Expand Down
14 changes: 5 additions & 9 deletions crates/uv-auth/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,9 @@ impl AuthMiddleware {
.as_ref()
.is_ok_and(|response| response.error_for_status_ref().is_ok())
{
if let (Some(index_url), Some(keyring)) = (index_url, &self.keyring) {
keyring.store_if_native(index_url, &credentials).await;
if let Some(keyring) = &self.keyring {
let url = index_url.unwrap_or(&url);
keyring.store_if_native(url, &credentials).await;
}
trace!("Updating cached credentials for {url} to {credentials:?}");
self.cache().insert(&url, credentials);
Expand Down Expand Up @@ -575,7 +576,7 @@ impl AuthMiddleware {
// 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 {
} else { 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
Expand Down Expand Up @@ -603,12 +604,7 @@ impl AuthMiddleware {
}
}
None => None,
} {
debug!("Found credentials in keyring for {url}");
Some(credentials)
} else {
None
}
} }
.map(Arc::new);

// Register the fetch for this key
Expand Down
3 changes: 1 addition & 2 deletions crates/uv-cache-key/src/canonical_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;

use url::Url;
use uv_redacted::DisplaySafeUrl;

use crate::cache_key::{CacheKey, CacheKeyHasher};
Expand Down Expand Up @@ -186,7 +185,7 @@ impl Hash for RepositoryUrl {
}

impl Deref for RepositoryUrl {
type Target = Url;
type Target = DisplaySafeUrl;

fn deref(&self) -> &Self::Target {
&self.0
Expand Down
10 changes: 10 additions & 0 deletions crates/uv-client/src/base_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use url::ParseError;
use url::Url;

use uv_auth::Credentials;
use uv_auth::KeyringProvider;
use uv_auth::{AuthMiddleware, Indexes};
use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified;
Expand Down Expand Up @@ -325,6 +326,7 @@ impl<'a> BaseClientBuilder<'a> {
dangerous_client,
raw_dangerous_client,
timeout,
keyring_provider: self.keyring.to_provider(),
}
}

Expand All @@ -351,6 +353,7 @@ impl<'a> BaseClientBuilder<'a> {
raw_client: existing.raw_client.clone(),
raw_dangerous_client: existing.raw_dangerous_client.clone(),
timeout: existing.timeout,
keyring_provider: self.keyring.to_provider(),
}
}

Expand Down Expand Up @@ -524,6 +527,8 @@ pub struct BaseClient {
allow_insecure_host: Vec<TrustedHost>,
/// The number of retries to attempt on transient errors.
retries: u32,
/// Backend for providing credentials from a keyring.
keyring_provider: Option<KeyringProvider>,
}

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -571,6 +576,11 @@ impl BaseClient {
pub fn retry_policy(&self) -> ExponentialBackoff {
ExponentialBackoff::builder().build_with_max_retries(self.retries)
}

/// The [`KeyringProvider`] if one exists.
pub fn keyring_provider(&self) -> &Option<KeyringProvider> {
&self.keyring_provider
}
}

/// Wrapper around [`ClientWithMiddleware`] that manages redirects.
Expand Down
18 changes: 18 additions & 0 deletions crates/uv-distribution/src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
self.reporter
.clone()
.map(|reporter| reporter.into_git_reporter()),
client
.unmanaged
.cached_client()
.uncached()
.keyring_provider()
.as_ref(),
)
.await?;

Expand Down Expand Up @@ -1763,6 +1769,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
self.reporter
.clone()
.map(|reporter| reporter.into_git_reporter()),
client
.unmanaged
.cached_client()
.uncached()
.keyring_provider()
.as_ref(),
)
.await?;

Expand Down Expand Up @@ -2010,6 +2022,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
self.reporter
.clone()
.map(|reporter| reporter.into_git_reporter()),
client
.unmanaged
.cached_client()
.uncached()
.keyring_provider()
.as_ref(),
)
.await?;

Expand Down
26 changes: 22 additions & 4 deletions crates/uv-git/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
use tracing::trace;
use uv_auth::Credentials;
use uv_auth::{Credentials, KeyringProvider};
use uv_cache_key::RepositoryUrl;
use uv_redacted::DisplaySafeUrl;

Expand All @@ -16,7 +16,20 @@ pub struct GitStore(RwLock<HashMap<RepositoryUrl, Arc<Credentials>>>);

impl GitStore {
/// Insert [`Credentials`] for the given URL into the store.
pub fn insert(&self, url: RepositoryUrl, credentials: Credentials) -> Option<Arc<Credentials>> {
///
/// If a native keyring provider is available, the credentials will also be
/// persisted to the system keyring for future use.
///
/// Returns the previously stored credentials for this URL, if any.
pub async fn insert(
Copy link
Copy Markdown
Contributor Author

@jtfmumm jtfmumm Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was switched to async for calling KeyringProvider::store_if_native, as was store_credentials_from_url below.

&self,
url: RepositoryUrl,
credentials: Credentials,
keyring_provider: Option<&KeyringProvider>,
) -> Option<Arc<Credentials>> {
if let Some(keyring_provider) = keyring_provider {
keyring_provider.store_if_native(&url, &credentials).await;
}
self.0.write().unwrap().insert(url, Arc::new(credentials))
}

Expand All @@ -29,10 +42,15 @@ impl GitStore {
/// Populate the global authentication store with credentials on a Git URL, if there are any.
///
/// Returns `true` if the store was updated.
pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool {
pub async fn store_credentials_from_url(
url: &DisplaySafeUrl,
keyring_provider: Option<&KeyringProvider>,
) -> bool {
if let Some(credentials) = Credentials::from_url(url) {
trace!("Caching credentials for {url}");
GIT_STORE.insert(RepositoryUrl::new(url), credentials);
GIT_STORE
.insert(RepositoryUrl::new(url), credentials, keyring_provider)
.await;
true
} else {
false
Expand Down
6 changes: 3 additions & 3 deletions crates/uv-git/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ impl GitCheckout {
/// The `remote_url` argument is the git remote URL where we want to fetch from.
fn fetch(
repo: &mut GitRepository,
remote_url: &Url,
remote_url: &DisplaySafeUrl,
reference: ReferenceOrOid<'_>,
client: &ClientWithMiddleware,
disable_ssl: bool,
Expand Down Expand Up @@ -601,7 +601,7 @@ fn fetch(
/// Attempts to use `git` CLI installed on the system to fetch a repository.
fn fetch_with_cli(
repo: &mut GitRepository,
url: &Url,
url: &DisplaySafeUrl,
refspecs: &[String],
tags: bool,
disable_ssl: bool,
Expand Down Expand Up @@ -733,7 +733,7 @@ enum FastPathRev {
/// [^1]: <https://developer.github.com/v3/repos/commits/#get-the-sha-1-of-a-commit-reference>
fn github_fast_path(
git: &mut GitRepository,
url: &Url,
url: &DisplaySafeUrl,
reference: ReferenceOrOid<'_>,
client: &ClientWithMiddleware,
) -> Result<FastPathRev> {
Expand Down
7 changes: 5 additions & 2 deletions crates/uv-git/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use fs_err::tokio as fs;
use reqwest_middleware::ClientWithMiddleware;
use tracing::debug;

use uv_auth::KeyringProvider;
use uv_cache_key::{RepositoryUrl, cache_digest};
use uv_fs::LockedFile;
use uv_git_types::{GitHubRepository, GitOid, GitReference, GitUrl};
Expand Down Expand Up @@ -148,6 +149,7 @@ impl GitResolver {
offline: bool,
cache: PathBuf,
reporter: Option<Arc<dyn Reporter>>,
keyring_provider: Option<&KeyringProvider>,
) -> Result<Fetch, GitResolverError> {
debug!("Fetching source distribution from Git: {url}");

Expand Down Expand Up @@ -187,8 +189,9 @@ impl GitResolver {
source
};

let fetch = tokio::task::spawn_blocking(move || source.fetch())
.await?
let fetch = source
.fetch(keyring_provider)
.await
.map_err(GitResolverError::Git)?;

// Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches
Expand Down
Loading
Loading