mirror of https://github.com/astral-sh/uv
Refactor `AuthenticationStore` to inline credentials (#2427)
This commit is contained in:
parent
9159731792
commit
22a52391be
|
|
@ -4277,7 +4277,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"base64 0.21.7",
|
||||
"clap",
|
||||
"lazy_static",
|
||||
"once_cell",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"rust-netrc",
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ indicatif = { version = "0.17.7" }
|
|||
indoc = { version = "2.0.4" }
|
||||
itertools = { version = "0.12.1" }
|
||||
junction = { version = "1.0.0" }
|
||||
lazy_static = { version = "1.4.0" }
|
||||
mailparse = { version = "0.14.0" }
|
||||
miette = { version = "6.0.0" }
|
||||
nanoid = { version = "0.4.0" }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use thiserror::Error;
|
|||
use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError};
|
||||
use pypi_types::{DistInfoMetadata, Hashes, Yanked};
|
||||
use url::Url;
|
||||
use uv_auth::AuthenticationStore;
|
||||
use uv_auth::GLOBAL_AUTH_STORE;
|
||||
|
||||
/// Error converting [`pypi_types::File`] to [`distribution_type::File`].
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -55,7 +55,7 @@ impl File {
|
|||
url: if file.url.contains("://") {
|
||||
let url = Url::parse(&file.url)
|
||||
.map_err(|err| FileConversionError::Url(file.url.clone(), err))?;
|
||||
let url = AuthenticationStore::with_url_encoded_auth(url);
|
||||
let url = GLOBAL_AUTH_STORE.with_url_encoded_auth(url);
|
||||
FileLocation::AbsoluteUrl(url.to_string())
|
||||
} else {
|
||||
FileLocation::RelativeUrl(base.to_string(), file.url)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ edition = "2021"
|
|||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "env"], optional = true }
|
||||
lazy_static = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
rust-netrc = { workspace = true }
|
||||
|
|
@ -15,6 +14,7 @@ task-local-extensions = { workspace = true }
|
|||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ mod store;
|
|||
|
||||
pub use keyring::KeyringProvider;
|
||||
pub use middleware::AuthMiddleware;
|
||||
use once_cell::sync::Lazy;
|
||||
pub use store::AuthenticationStore;
|
||||
|
||||
use url::Url;
|
||||
|
||||
// TODO(zanieb): Consider passing a store explicitly throughout
|
||||
|
||||
/// Global authentication store for a `uv` invocation
|
||||
pub static GLOBAL_AUTH_STORE: Lazy<AuthenticationStore> = Lazy::new(AuthenticationStore::default);
|
||||
|
||||
/// Used to determine if authentication information should be retained on a new URL.
|
||||
/// Based on the specification defined in RFC 7235 and 7230.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ use tracing::{debug, warn};
|
|||
|
||||
use crate::{
|
||||
keyring::{get_keyring_subprocess_auth, KeyringProvider},
|
||||
store::{AuthenticationStore, Credential},
|
||||
store::Credential,
|
||||
GLOBAL_AUTH_STORE,
|
||||
};
|
||||
|
||||
/// A middleware that adds basic authentication to requests based on the netrc file and the keyring.
|
||||
|
|
@ -47,13 +48,13 @@ impl Middleware for AuthMiddleware {
|
|||
// This gives in-URL credentials precedence over the netrc file.
|
||||
if req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
||||
if !url.username().is_empty() {
|
||||
AuthenticationStore::save_from_url(&url);
|
||||
GLOBAL_AUTH_STORE.save_from_url(&url);
|
||||
}
|
||||
return next.run(req, _extensions).await;
|
||||
}
|
||||
|
||||
// Try auth strategies in order of precedence:
|
||||
if let Some(stored_auth) = AuthenticationStore::get(&url) {
|
||||
if let Some(stored_auth) = GLOBAL_AUTH_STORE.get(&url) {
|
||||
// If we've already seen this URL, we can use the stored credentials
|
||||
if let Some(auth) = stored_auth {
|
||||
match auth {
|
||||
|
|
@ -77,7 +78,7 @@ impl Middleware for AuthMiddleware {
|
|||
reqwest::header::AUTHORIZATION,
|
||||
basic_auth(auth.username(), auth.password()),
|
||||
);
|
||||
AuthenticationStore::set(&url, Some(auth));
|
||||
GLOBAL_AUTH_STORE.set(&url, Some(auth));
|
||||
} else if matches!(self.keyring_provider, KeyringProvider::Subprocess) {
|
||||
// If we have keyring support enabled, we check there as well
|
||||
match get_keyring_subprocess_auth(&url) {
|
||||
|
|
@ -86,7 +87,7 @@ impl Middleware for AuthMiddleware {
|
|||
reqwest::header::AUTHORIZATION,
|
||||
basic_auth(auth.username(), auth.password()),
|
||||
);
|
||||
AuthenticationStore::set(&url, Some(auth));
|
||||
GLOBAL_AUTH_STORE.set(&url, Some(auth));
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!("No keyring credentials found for {url}");
|
||||
|
|
@ -99,7 +100,7 @@ impl Middleware for AuthMiddleware {
|
|||
|
||||
// If we still don't have any credentials, we save the URL so we don't have to check netrc or keyring again
|
||||
if !req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
||||
AuthenticationStore::set(&url, None);
|
||||
GLOBAL_AUTH_STORE.set(&url, None);
|
||||
}
|
||||
|
||||
next.run(req, _extensions).await
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use lazy_static::lazy_static;
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
use netrc::Authenticator;
|
||||
|
|
@ -7,11 +6,6 @@ use url::Url;
|
|||
|
||||
use crate::NetLoc;
|
||||
|
||||
lazy_static! {
|
||||
// Store credentials for NetLoc
|
||||
static ref PASSWORDS: Mutex<HashMap<NetLoc, Option<Credential>>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Credential {
|
||||
Basic(BasicAuthData),
|
||||
|
|
@ -68,35 +62,49 @@ pub struct BasicAuthData {
|
|||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AuthenticationStore;
|
||||
pub struct AuthenticationStore {
|
||||
credentials: Mutex<HashMap<NetLoc, Option<Credential>>>,
|
||||
}
|
||||
|
||||
impl Default for AuthenticationStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticationStore {
|
||||
pub fn get(url: &Url) -> Option<Option<Credential>> {
|
||||
let netloc = NetLoc::from(url);
|
||||
let passwords = PASSWORDS.lock().unwrap();
|
||||
passwords.get(&netloc).cloned()
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
credentials: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(url: &Url, auth: Option<Credential>) {
|
||||
pub fn get(&self, url: &Url) -> Option<Option<Credential>> {
|
||||
let netloc = NetLoc::from(url);
|
||||
let mut passwords = PASSWORDS.lock().unwrap();
|
||||
passwords.insert(netloc, auth);
|
||||
let credentials = self.credentials.lock().unwrap();
|
||||
credentials.get(&netloc).cloned()
|
||||
}
|
||||
|
||||
pub fn set(&self, url: &Url, auth: Option<Credential>) {
|
||||
let netloc = NetLoc::from(url);
|
||||
let mut credentials = self.credentials.lock().unwrap();
|
||||
credentials.insert(netloc, auth);
|
||||
}
|
||||
|
||||
/// Copy authentication from one URL to another URL if applicable.
|
||||
pub fn with_url_encoded_auth(url: Url) -> Url {
|
||||
pub fn with_url_encoded_auth(&self, url: Url) -> Url {
|
||||
let netloc = NetLoc::from(&url);
|
||||
let passwords = PASSWORDS.lock().unwrap();
|
||||
if let Some(Some(Credential::UrlEncoded(url_auth))) = passwords.get(&netloc) {
|
||||
let credentials = self.credentials.lock().unwrap();
|
||||
if let Some(Some(Credential::UrlEncoded(url_auth))) = credentials.get(&netloc) {
|
||||
url_auth.apply_to_url(url)
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_from_url(url: &Url) {
|
||||
pub fn save_from_url(&self, url: &Url) {
|
||||
let netloc = NetLoc::from(url);
|
||||
let mut passwords = PASSWORDS.lock().unwrap();
|
||||
let mut credentials = self.credentials.lock().unwrap();
|
||||
if url.username().is_empty() {
|
||||
// No credentials to save
|
||||
return;
|
||||
|
|
@ -105,7 +113,7 @@ impl AuthenticationStore {
|
|||
username: url.username().to_string(),
|
||||
password: url.password().map(str::to_string),
|
||||
};
|
||||
passwords.insert(netloc, Some(Credential::UrlEncoded(auth)));
|
||||
credentials.insert(netloc, Some(Credential::UrlEncoded(auth)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,63 +121,76 @@ impl AuthenticationStore {
|
|||
mod test {
|
||||
use super::*;
|
||||
|
||||
// NOTE: Because tests run in parallel, it is imperative to use different URLs for each
|
||||
#[test]
|
||||
fn set_get_work() {
|
||||
let url = Url::parse("https://test1example1.com/simple/").unwrap();
|
||||
let not_set_res = AuthenticationStore::get(&url);
|
||||
fn store_set_and_get() {
|
||||
let store = AuthenticationStore::new();
|
||||
let url = Url::parse("https://example1.com/simple/").unwrap();
|
||||
let not_set_res = store.get(&url);
|
||||
assert!(not_set_res.is_none());
|
||||
|
||||
let found_first_url = Url::parse("https://test1example2.com/simple/first/").unwrap();
|
||||
let not_found_first_url = Url::parse("https://test1example3.com/simple/first/").unwrap();
|
||||
let found_first_url = Url::parse("https://example2.com/simple/first/").unwrap();
|
||||
let not_found_first_url = Url::parse("https://example3.com/simple/first/").unwrap();
|
||||
|
||||
AuthenticationStore::set(
|
||||
store.set(
|
||||
&found_first_url,
|
||||
Some(Credential::Basic(BasicAuthData {
|
||||
username: "u".to_string(),
|
||||
password: Some("p".to_string()),
|
||||
})),
|
||||
);
|
||||
AuthenticationStore::set(¬_found_first_url, None);
|
||||
store.set(¬_found_first_url, None);
|
||||
|
||||
let found_second_url = Url::parse("https://test1example2.com/simple/second/").unwrap();
|
||||
let not_found_second_url = Url::parse("https://test1example3.com/simple/second/").unwrap();
|
||||
let found_second_url = Url::parse("https://example2.com/simple/second/").unwrap();
|
||||
let not_found_second_url = Url::parse("https://example3.com/simple/second/").unwrap();
|
||||
|
||||
let found_res = AuthenticationStore::get(&found_second_url);
|
||||
let found_res = store.get(&found_second_url);
|
||||
assert!(found_res.is_some());
|
||||
let found_res = found_res.unwrap();
|
||||
assert!(matches!(found_res, Some(Credential::Basic(_))));
|
||||
|
||||
let not_found_res = AuthenticationStore::get(¬_found_second_url);
|
||||
let not_found_res = store.get(¬_found_second_url);
|
||||
assert!(not_found_res.is_some());
|
||||
let not_found_res = not_found_res.unwrap();
|
||||
assert!(not_found_res.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_url_encoded_auth_works() {
|
||||
let url = Url::parse("https://test2example.com/simple/").unwrap();
|
||||
fn store_with_url_encoded_auth() {
|
||||
let store = AuthenticationStore::new();
|
||||
let url = Url::parse("https://example.com/simple/").unwrap();
|
||||
let auth = Credential::UrlEncoded(UrlAuthData {
|
||||
username: "u".to_string(),
|
||||
password: Some("p".to_string()),
|
||||
});
|
||||
|
||||
AuthenticationStore::set(&url, Some(auth.clone()));
|
||||
// Before adding to the store there's no change
|
||||
let url = store.with_url_encoded_auth(url);
|
||||
assert_eq!(url.username(), "");
|
||||
assert_eq!(url.password(), None);
|
||||
|
||||
let url = AuthenticationStore::with_url_encoded_auth(url);
|
||||
store.set(&url, Some(auth.clone()));
|
||||
|
||||
// After adding to the store, the url is updated
|
||||
let url = store.with_url_encoded_auth(url);
|
||||
assert_eq!(url.username(), "u");
|
||||
assert_eq!(url.password(), Some("p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_from_url_works() {
|
||||
let url = Url::parse("https://u:p@test3example.com/simple/").unwrap();
|
||||
fn store_save_from_url() {
|
||||
let store = AuthenticationStore::new();
|
||||
let url = Url::parse("https://u:p@example.com/simple/").unwrap();
|
||||
|
||||
AuthenticationStore::save_from_url(&url);
|
||||
store.save_from_url(&url);
|
||||
|
||||
let found_res = AuthenticationStore::get(&url);
|
||||
let found_res = store.get(&url);
|
||||
assert!(found_res.is_some());
|
||||
let found_res = found_res.unwrap();
|
||||
assert!(matches!(found_res, Some(Credential::UrlEncoded(_))));
|
||||
|
||||
let url = Url::parse("https://example2.com/simple/").unwrap();
|
||||
store.save_from_url(&url);
|
||||
let found_res = store.get(&url);
|
||||
assert!(found_res.is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use pep440_rs::Version;
|
|||
use pep508_rs::VerbatimUrl;
|
||||
use platform_tags::Tags;
|
||||
use pypi_types::Hashes;
|
||||
use uv_auth::AuthenticationStore;
|
||||
use uv_auth::GLOBAL_AUTH_STORE;
|
||||
use uv_cache::{Cache, CacheBucket};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
|
|
@ -157,13 +157,13 @@ impl<'a> FlatIndexClient<'a> {
|
|||
async {
|
||||
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
||||
// This ensures that we handle redirects and other URL transformations correctly.
|
||||
let url = AuthenticationStore::with_url_encoded_auth(response.url().clone());
|
||||
let url = GLOBAL_AUTH_STORE.with_url_encoded_auth(response.url().clone());
|
||||
|
||||
let text = response.text().await.map_err(ErrorKind::from)?;
|
||||
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
||||
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
||||
|
||||
let base = AuthenticationStore::with_url_encoded_auth(base.into_url());
|
||||
let base = GLOBAL_AUTH_STORE.with_url_encoded_auth(base.into_url());
|
||||
let files: Vec<File> = files
|
||||
.into_iter()
|
||||
.filter_map(|file| {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use distribution_types::{BuiltDist, File, FileLocation, IndexUrl, IndexUrls, Nam
|
|||
use install_wheel_rs::metadata::{find_archive_dist_info, is_metadata_entry};
|
||||
use pep440_rs::Version;
|
||||
use pypi_types::{Metadata23, SimpleJson};
|
||||
use uv_auth::{AuthMiddleware, AuthenticationStore, KeyringProvider};
|
||||
use uv_auth::{AuthMiddleware, KeyringProvider, GLOBAL_AUTH_STORE};
|
||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::PackageName;
|
||||
|
|
@ -317,7 +317,7 @@ impl RegistryClient {
|
|||
async {
|
||||
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
||||
// This ensures that we handle redirects and other URL transformations correctly.
|
||||
let url = AuthenticationStore::with_url_encoded_auth(response.url().clone());
|
||||
let url = GLOBAL_AUTH_STORE.with_url_encoded_auth(response.url().clone());
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
|
|
@ -346,7 +346,7 @@ impl RegistryClient {
|
|||
let text = response.text().await.map_err(ErrorKind::from)?;
|
||||
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
||||
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
||||
let base = AuthenticationStore::with_url_encoded_auth(base.into_url());
|
||||
let base = GLOBAL_AUTH_STORE.with_url_encoded_auth(base.into_url());
|
||||
|
||||
SimpleMetadata::from_files(files, package_name, &base)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue