Make KeyringProvider::fetch_* async (#3089)

To resolve #3073
This commit is contained in:
哇呜哇呜呀咦耶 2024-04-23 20:58:00 +08:00 committed by GitHub
parent ad923b71a7
commit 65efaf70da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 59 deletions

1
Cargo.lock generated
View File

@ -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",

View File

@ -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 }

View File

@ -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);
} }
} }

View File

@ -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 {