mirror of https://github.com/astral-sh/uv
parent
ad923b71a7
commit
65efaf70da
|
|
@ -4489,6 +4489,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.0",
|
"base64 0.22.0",
|
||||||
|
"futures",
|
||||||
"http",
|
"http",
|
||||||
"insta",
|
"insta",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
|
|
@ -16,6 +17,7 @@ schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
urlencoding = { workspace = true }
|
urlencoding = { workspace = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{collections::HashSet, process::Command, sync::Mutex};
|
use std::{collections::HashSet, sync::Mutex};
|
||||||
|
|
||||||
|
use tokio::process::Command;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ impl KeyringProvider {
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if no password was found for the username or if any errors
|
/// Returns [`None`] if no password was found for the username or if any errors
|
||||||
/// are encountered in the keyring backend.
|
/// are encountered in the keyring backend.
|
||||||
pub(crate) fn fetch(&self, url: &Url, username: &str) -> Option<Credentials> {
|
pub(crate) async fn fetch(&self, url: &Url, username: &str) -> Option<Credentials> {
|
||||||
// Validate the request
|
// Validate the request
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
url.host_str().is_some(),
|
url.host_str().is_some(),
|
||||||
|
|
@ -61,20 +62,24 @@ impl KeyringProvider {
|
||||||
// This behavior avoids adding ~80ms to every request when the subprocess keyring
|
// This behavior avoids adding ~80ms to every request when the subprocess keyring
|
||||||
// provider is being used, but makes assumptions about the typical keyring
|
// provider is being used, but makes assumptions about the typical keyring
|
||||||
// use-cases.
|
// use-cases.
|
||||||
let mut cache = self.cache.lock().unwrap();
|
|
||||||
|
|
||||||
let key = (host.to_string(), username.to_string());
|
let key = (host.to_string(), username.to_string());
|
||||||
if cache.contains(&key) {
|
{
|
||||||
debug!(
|
let cache = self.cache.lock().unwrap();
|
||||||
|
|
||||||
|
if cache.contains(&key) {
|
||||||
|
debug!(
|
||||||
"Skipping keyring lookup for {username} at {host}, already attempted and found no credentials."
|
"Skipping keyring lookup for {username} at {host}, already attempted and found no credentials."
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the full URL first
|
// Check the full URL first
|
||||||
// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L376C1-L379C14>
|
// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L376C1-L379C14>
|
||||||
let mut password = match self.backend {
|
let mut password = match self.backend {
|
||||||
KeyringProviderBackend::Subprocess => self.fetch_subprocess(url.as_str(), username),
|
KeyringProviderBackend::Subprocess => {
|
||||||
|
self.fetch_subprocess(url.as_str(), username).await
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
KeyringProviderBackend::Dummy(ref store) => {
|
KeyringProviderBackend::Dummy(ref store) => {
|
||||||
self.fetch_dummy(store, url.as_str(), username)
|
self.fetch_dummy(store, url.as_str(), username)
|
||||||
|
|
@ -83,26 +88,30 @@ impl KeyringProvider {
|
||||||
// And fallback to a check for the host
|
// And fallback to a check for the host
|
||||||
if password.is_none() {
|
if password.is_none() {
|
||||||
password = match self.backend {
|
password = match self.backend {
|
||||||
KeyringProviderBackend::Subprocess => self.fetch_subprocess(host, username),
|
KeyringProviderBackend::Subprocess => self.fetch_subprocess(host, username).await,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
KeyringProviderBackend::Dummy(ref store) => self.fetch_dummy(store, host, username),
|
KeyringProviderBackend::Dummy(ref store) => self.fetch_dummy(store, host, username),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if password.is_none() {
|
{
|
||||||
cache.insert(key);
|
let mut cache = self.cache.lock().unwrap();
|
||||||
|
if password.is_none() {
|
||||||
|
cache.insert(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
password.map(|password| Credentials::new(Some(username.to_string()), Some(password)))
|
password.map(|password| Credentials::new(Some(username.to_string()), Some(password)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
fn fetch_subprocess(&self, service_name: &str, username: &str) -> Option<String> {
|
async fn fetch_subprocess(&self, service_name: &str, username: &str) -> Option<String> {
|
||||||
let output = Command::new("keyring")
|
let output = Command::new("keyring")
|
||||||
.arg("get")
|
.arg("get")
|
||||||
.arg(service_name)
|
.arg(service_name)
|
||||||
.arg(username)
|
.arg(username)
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.inspect_err(|err| warn!("Failure running `keyring` command: {err}"))
|
.inspect_err(|err| warn!("Failure running `keyring` command: {err}"))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
|
|
@ -161,55 +170,62 @@ impl KeyringProvider {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use futures::FutureExt;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_no_host() {
|
async fn fetch_url_no_host() {
|
||||||
let url = Url::parse("file:/etc/bin/").unwrap();
|
let url = Url::parse("file:/etc/bin/").unwrap();
|
||||||
let keyring = KeyringProvider::empty();
|
let keyring = KeyringProvider::empty();
|
||||||
// Panics due to debug assertion; returns `None` in production
|
// Panics due to debug assertion; returns `None` in production
|
||||||
let result = std::panic::catch_unwind(|| keyring.fetch(&url, "user"));
|
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, "user"))
|
||||||
|
.catch_unwind()
|
||||||
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_with_password() {
|
async fn fetch_url_with_password() {
|
||||||
let url = Url::parse("https://user:password@example.com").unwrap();
|
let url = Url::parse("https://user:password@example.com").unwrap();
|
||||||
let keyring = KeyringProvider::empty();
|
let keyring = KeyringProvider::empty();
|
||||||
// Panics due to debug assertion; returns `None` in production
|
// Panics due to debug assertion; returns `None` in production
|
||||||
let result = std::panic::catch_unwind(|| keyring.fetch(&url, url.username()));
|
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username()))
|
||||||
|
.catch_unwind()
|
||||||
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_with_no_username() {
|
async fn fetch_url_with_no_username() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::empty();
|
let keyring = KeyringProvider::empty();
|
||||||
// Panics due to debug assertion; returns `None` in production
|
// Panics due to debug assertion; returns `None` in production
|
||||||
let result = std::panic::catch_unwind(|| keyring.fetch(&url, url.username()));
|
let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username()))
|
||||||
|
.catch_unwind()
|
||||||
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_no_auth() {
|
async fn fetch_url_no_auth() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::empty();
|
let keyring = KeyringProvider::empty();
|
||||||
let credentials = keyring.fetch(&url, "user");
|
let credentials = keyring.fetch(&url, "user");
|
||||||
assert!(credentials.is_none());
|
assert!(credentials.await.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url() {
|
async fn fetch_url() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keyring.fetch(&url, "user"),
|
keyring.fetch(&url, "user").await,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
Some("password".to_string())
|
Some("password".to_string())
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keyring.fetch(&url.join("test").unwrap(), "user"),
|
keyring.fetch(&url.join("test").unwrap(), "user").await,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
Some("password".to_string())
|
Some("password".to_string())
|
||||||
|
|
@ -217,37 +233,37 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_no_match() {
|
async fn fetch_url_no_match() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::dummy([(("other.com", "user"), "password")]);
|
let keyring = KeyringProvider::dummy([(("other.com", "user"), "password")]);
|
||||||
let credentials = keyring.fetch(&url, "user");
|
let credentials = keyring.fetch(&url, "user").await;
|
||||||
assert_eq!(credentials, None);
|
assert_eq!(credentials, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_prefers_url_to_host() {
|
async fn fetch_url_prefers_url_to_host() {
|
||||||
let url = Url::parse("https://example.com/").unwrap();
|
let url = Url::parse("https://example.com/").unwrap();
|
||||||
let keyring = KeyringProvider::dummy([
|
let keyring = KeyringProvider::dummy([
|
||||||
((url.join("foo").unwrap().as_str(), "user"), "password"),
|
((url.join("foo").unwrap().as_str(), "user"), "password"),
|
||||||
((url.host_str().unwrap(), "user"), "other-password"),
|
((url.host_str().unwrap(), "user"), "other-password"),
|
||||||
]);
|
]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keyring.fetch(&url.join("foo").unwrap(), "user"),
|
keyring.fetch(&url.join("foo").unwrap(), "user").await,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
Some("password".to_string())
|
Some("password".to_string())
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keyring.fetch(&url, "user"),
|
keyring.fetch(&url, "user").await,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
Some("other-password".to_string())
|
Some("other-password".to_string())
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keyring.fetch(&url.join("bar").unwrap(), "user"),
|
keyring.fetch(&url.join("bar").unwrap(), "user").await,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
Some("other-password".to_string())
|
Some("other-password".to_string())
|
||||||
|
|
@ -258,24 +274,24 @@ mod test {
|
||||||
/// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of
|
/// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of
|
||||||
/// credentials for _every_ request URL at the cost of inconsistent behavior when
|
/// credentials for _every_ request URL at the cost of inconsistent behavior when
|
||||||
/// credentials are not scoped to a realm.
|
/// credentials are not scoped to a realm.
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_caches_based_on_host() {
|
async fn fetch_url_caches_based_on_host() {
|
||||||
let url = Url::parse("https://example.com/").unwrap();
|
let url = Url::parse("https://example.com/").unwrap();
|
||||||
let keyring =
|
let keyring =
|
||||||
KeyringProvider::dummy([((url.join("foo").unwrap().as_str(), "user"), "password")]);
|
KeyringProvider::dummy([((url.join("foo").unwrap().as_str(), "user"), "password")]);
|
||||||
|
|
||||||
// If we attempt an unmatching URL first...
|
// If we attempt an unmatching URL first...
|
||||||
assert_eq!(keyring.fetch(&url.join("bar").unwrap(), "user"), None);
|
assert_eq!(keyring.fetch(&url.join("bar").unwrap(), "user").await, None);
|
||||||
|
|
||||||
// ... we will cache the missing credentials on subsequent attempts
|
// ... we will cache the missing credentials on subsequent attempts
|
||||||
assert_eq!(keyring.fetch(&url.join("foo").unwrap(), "user"), None);
|
assert_eq!(keyring.fetch(&url.join("foo").unwrap(), "user").await, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_username() {
|
async fn fetch_url_username() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
||||||
let credentials = keyring.fetch(&url, "user");
|
let credentials = keyring.fetch(&url, "user").await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
credentials,
|
credentials,
|
||||||
Some(Credentials::new(
|
Some(Credentials::new(
|
||||||
|
|
@ -285,16 +301,16 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn fetch_url_username_no_match() {
|
async fn fetch_url_username_no_match() {
|
||||||
let url = Url::parse("https://example.com").unwrap();
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "foo"), "password")]);
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "foo"), "password")]);
|
||||||
let credentials = keyring.fetch(&url, "bar");
|
let credentials = keyring.fetch(&url, "bar").await;
|
||||||
assert_eq!(credentials, None);
|
assert_eq!(credentials, None);
|
||||||
|
|
||||||
// Still fails if we have `foo` in the URL itself
|
// Still fails if we have `foo` in the URL itself
|
||||||
let url = Url::parse("https://foo@example.com").unwrap();
|
let url = Url::parse("https://foo@example.com").unwrap();
|
||||||
let credentials = keyring.fetch(&url, "bar");
|
let credentials = keyring.fetch(&url, "bar").await;
|
||||||
assert_eq!(credentials, None);
|
assert_eq!(credentials, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -318,18 +318,19 @@ impl AuthMiddleware {
|
||||||
// falls back to the host, but we cache the result per host so if a keyring
|
// falls back to the host, but we cache the result per host so if a keyring
|
||||||
// implementation returns different credentials for different URLs in the
|
// implementation returns different credentials for different URLs in the
|
||||||
// same realm we will use the wrong credentials.
|
// same realm we will use the wrong credentials.
|
||||||
} else if let Some(credentials) = self.keyring.as_ref().and_then(|keyring| {
|
} else if let Some(credentials) = match self.keyring {
|
||||||
if let Some(username) = credentials
|
Some(ref keyring) => match credentials.and_then(|credentials| credentials.username()) {
|
||||||
.as_ref()
|
Some(username) => {
|
||||||
.and_then(|credentials| credentials.username())
|
debug!("Checking keyring for credentials for {username}@{url}");
|
||||||
{
|
keyring.fetch(url, username).await
|
||||||
debug!("Checking keyring for credentials for {username}@{url}");
|
}
|
||||||
keyring.fetch(url, username)
|
None => {
|
||||||
} else {
|
trace!("Skipping keyring lookup for {url} with no username");
|
||||||
trace!("Skipping keyring lookup for {url} with no username");
|
None
|
||||||
None
|
}
|
||||||
}
|
},
|
||||||
}) {
|
None => None,
|
||||||
|
} {
|
||||||
debug!("Found credentials in keyring for {url}");
|
debug!("Found credentials in keyring for {url}");
|
||||||
Some(credentials)
|
Some(credentials)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue