Skip to content
Closed
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ uv-git-types = { path = "crates/uv-git-types" }
uv-globfilter = { path = "crates/uv-globfilter" }
uv-install-wheel = { path = "crates/uv-install-wheel", default-features = false }
uv-installer = { path = "crates/uv-installer" }
uv-keyring = { path = "crates/uv-keyring" }
uv-macros = { path = "crates/uv-macros" }
uv-metadata = { path = "crates/uv-metadata" }
uv-normalize = { path = "crates/uv-normalize" }
Expand Down
1 change: 1 addition & 0 deletions crates/uv-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ doctest = false
workspace = true

[dependencies]
uv-keyring = { workspace = true, features = ["apple-native", "secret-service", "windows-native"] }
uv-once-map = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true }
Expand Down
128 changes: 125 additions & 3 deletions crates/uv-auth/src/keyring.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use std::{io::Write, process::Stdio};
use rustc_hash::FxHashSet;
use std::{
io::Write,
process::Stdio,
sync::{LazyLock, RwLock},
};
use tokio::process::Command;
use tracing::{instrument, trace, warn};
use tracing::{debug, instrument, trace, warn};
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user_once;

use crate::credentials::Credentials;

/// Keyring credentials that have been stored during an invocation of uv.
static STORED_KEYRING_URLS: LazyLock<StoredKeyringUrls> = LazyLock::new(StoredKeyringUrls::new);
/// Service name prefix for storing credentials in a keyring.
static UV_SERVICE_PREFIX: &str = "uv-credentials:";

/// A backend for retrieving credentials from a keyring.
///
/// See pip's implementation for reference
Expand All @@ -17,20 +27,88 @@ pub struct KeyringProvider {

#[derive(Debug)]
pub(crate) enum KeyringProviderBackend {
/// Use the `keyring` command to fetch credentials.
/// Use system keyring integration to fetch credentials.
Native,
/// Use the external `keyring` command to fetch credentials.
Subprocess,
#[cfg(test)]
Dummy(Vec<(String, &'static str, &'static str)>),
}

impl KeyringProvider {
/// Create a new [`KeyringProvider::Native`].
pub fn native() -> Self {
Self {
backend: KeyringProviderBackend::Native,
}
}

/// Create a new [`KeyringProvider::Subprocess`].
pub fn subprocess() -> Self {
Self {
backend: KeyringProviderBackend::Subprocess,
}
}

/// 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))]
pub async fn store_if_native(&self, url: &DisplaySafeUrl, credentials: &Credentials) {
match &self.backend {
KeyringProviderBackend::Native => {
let Some(username) = credentials.username() else {
trace!(
"Unable to store credentials in keyring for {url} due to missing username"
);
return;
};
let Some(password) = credentials.password() else {
trace!(
"Unable to store credentials in keyring for {url} due to missing password"
);
return;
};

// Only store credentials if not already stored during this uv invocation.
if !STORED_KEYRING_URLS.contains(url) {
self.store_native(url.as_str(), username, password).await;
STORED_KEYRING_URLS.insert(url.clone());
}
}
KeyringProviderBackend::Subprocess => {
trace!("Storing credentials is not supported for `subprocess` keyring");
}
#[cfg(test)]
KeyringProviderBackend::Dummy(_) => {}
}
}

/// Store credentials to the system keyring for the given `service_name`/`username`
/// pair.
#[instrument(skip(self))]
async fn store_native(&self, service: &str, username: &str, password: &str) {
let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}");
let entry = match uv_keyring::Entry::new(&prefixed_service, username) {
Ok(entry) => entry,
Err(err) => {
warn_user_once!(
"Unable to store credentials for {service} in the system keyring: {err}"
);
return;
}
};
match entry.set_password(password).await {
Ok(()) => {
debug!("Storing credentials for {service} in system keyring");
}
Err(err) => {
warn_user_once!(
"Unable to store credentials for {service} in the system keyring: {err}"
);
}
}
}

/// Fetch credentials for the given [`Url`] from the keyring.
///
/// Returns [`None`] if no password was found for the username or if any errors
Expand All @@ -55,6 +133,7 @@ impl KeyringProvider {
// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L376C1-L379C14>
trace!("Checking keyring for URL {url}");
let mut credentials = match self.backend {
KeyringProviderBackend::Native => self.fetch_native(url.as_str(), username).await,
KeyringProviderBackend::Subprocess => {
self.fetch_subprocess(url.as_str(), username).await
}
Expand All @@ -72,6 +151,7 @@ impl KeyringProvider {
};
trace!("Checking keyring for host {host}");
credentials = match self.backend {
KeyringProviderBackend::Native => self.fetch_native(&host, username).await,
KeyringProviderBackend::Subprocess => self.fetch_subprocess(&host, username).await,
#[cfg(test)]
KeyringProviderBackend::Dummy(ref store) => {
Expand Down Expand Up @@ -175,6 +255,31 @@ impl KeyringProvider {
}
}

#[instrument(skip(self))]
async fn fetch_native(
&self,
service: &str,
username: Option<&str>,
) -> Option<(String, String)> {
let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}");
let username = username?;
if let Ok(entry) = uv_keyring::Entry::new(&prefixed_service, username) {
match entry.get_password().await {
Ok(password) => return Some((username.to_string(), password)),
Err(uv_keyring::Error::NoEntry) => {
debug!("No entry found in system keyring for {service}");
}
Err(err) => {
warn_user_once!(
"Unable to fetch credentials for {service} from system keyring: {}",
err
);
}
}
}
None
}

#[cfg(test)]
fn fetch_dummy(
store: &Vec<(String, &'static str, &'static str)>,
Expand Down Expand Up @@ -213,6 +318,23 @@ impl KeyringProvider {
}
}

/// Keyring credentials that have been stored during an invocation of uv.
struct StoredKeyringUrls(RwLock<FxHashSet<DisplaySafeUrl>>);

impl StoredKeyringUrls {
pub(crate) fn new() -> Self {
Self(RwLock::new(FxHashSet::default()))
}

pub(crate) fn contains(&self, url: &DisplaySafeUrl) -> bool {
self.0.read().unwrap().contains(url)
}

pub(crate) fn insert(&self, url: DisplaySafeUrl) -> bool {
self.0.write().unwrap().insert(url)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
61 changes: 54 additions & 7 deletions crates/uv-auth/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,14 @@ impl Middleware for AuthMiddleware {
if credentials.password().is_some() {
trace!("Request for {url} is fully authenticated");
return self
.complete_request(None, request, extensions, next, auth_policy)
.complete_request(
None,
request,
extensions,
next,
maybe_index_url,
auth_policy,
)
.await;
}

Expand Down Expand Up @@ -299,7 +306,14 @@ impl Middleware for AuthMiddleware {
trace!("Retrying request for {url} with credentials from cache {credentials:?}");
retry_request = credentials.authenticate(retry_request);
return self
.complete_request(None, retry_request, extensions, next, auth_policy)
.complete_request(
None,
retry_request,
extensions,
next,
maybe_index_url,
auth_policy,
)
.await;
}
}
Expand All @@ -323,6 +337,7 @@ impl Middleware for AuthMiddleware {
retry_request,
extensions,
next,
maybe_index_url,
auth_policy,
)
.await;
Expand All @@ -333,7 +348,14 @@ impl Middleware for AuthMiddleware {
trace!("Retrying request for {url} with username from cache {credentials:?}");
retry_request = credentials.authenticate(retry_request);
return self
.complete_request(None, retry_request, extensions, next, auth_policy)
.complete_request(
None,
retry_request,
extensions,
next,
maybe_index_url,
auth_policy,
)
.await;
}
}
Expand All @@ -358,6 +380,7 @@ impl AuthMiddleware {
request: Request,
extensions: &mut Extensions,
next: Next<'_>,
index_url: Option<&DisplaySafeUrl>,
auth_policy: AuthPolicy,
) -> reqwest_middleware::Result<Response> {
let Some(credentials) = credentials else {
Expand All @@ -375,6 +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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is currently only storing credentials on successful authentication if this is an index URL.

keyring.store_if_native(index_url, &credentials).await;
}
trace!("Updating cached credentials for {url} to {credentials:?}");
self.cache().insert(&url, credentials);
}
Expand All @@ -399,7 +425,14 @@ impl AuthMiddleware {
if credentials.password().is_some() {
trace!("Request for {url} already contains username and password");
return self
.complete_request(Some(credentials), request, extensions, next, auth_policy)
.complete_request(
Some(credentials),
request,
extensions,
next,
index_url,
auth_policy,
)
.await;
}

Expand All @@ -420,7 +453,14 @@ impl AuthMiddleware {
// Do not insert already-cached credentials
let credentials = None;
return self
.complete_request(credentials, request, extensions, next, auth_policy)
.complete_request(
credentials,
request,
extensions,
next,
index_url,
auth_policy,
)
.await;
}

Expand Down Expand Up @@ -458,8 +498,15 @@ impl AuthMiddleware {
Some(credentials)
};

self.complete_request(credentials, request, extensions, next, auth_policy)
.await
self.complete_request(
credentials,
request,
extensions,
next,
index_url,
auth_policy,
)
.await
}

/// Fetch credentials for a URL.
Expand Down
3 changes: 3 additions & 0 deletions crates/uv-configuration/src/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub enum KeyringProviderType {
/// Do not use keyring for credential lookup.
#[default]
Disabled,
/// Use the system keyring for credential lookup.
Native,
/// Use the `keyring` command for credential lookup.
Subprocess,
// /// Not yet implemented
Expand All @@ -22,6 +24,7 @@ impl KeyringProviderType {
pub fn to_provider(&self) -> Option<KeyringProvider> {
match self {
Self::Disabled => None,
Self::Native => Some(KeyringProvider::native()),
Self::Subprocess => Some(KeyringProvider::subprocess()),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ ignored = [

[features]
default = ["performance", "uv-distribution/static", "default-tests"]
keyring-tests = []
# Use better memory allocators, etc.
performance = ["performance-memory-allocator"]
performance-memory-allocator = ["dep:uv-performance-memory-allocator"]
Expand Down
Loading
Loading