mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
Rewrite uv-auth (#2976)
Closes - #2822 - https://github.com/astral-sh/uv/issues/2563 (via #2984) Partially address: - https://github.com/astral-sh/uv/issues/2465 - https://github.com/astral-sh/uv/issues/2464 Supersedes: - https://github.com/astral-sh/uv/pull/2947 - https://github.com/astral-sh/uv/pull/2570 (via #2984) Some significant refactors to the whole `uv-auth` crate: - Improving the API - Adding test coverage - Fixing handling of URL-encoded passwords - Fixing keyring authentication - Updated middleware (see #2984 for more)
This commit is contained in:
parent
193704f98b
commit
c0efeeddf6
22 changed files with 1493 additions and 568 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
|
@ -3731,6 +3731,27 @@ dependencies = [
|
||||||
"test-case-core",
|
"test-case-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b319995299c65d522680decf80f2c108d85b861d81dfe340a10d16cee29d9e6"
|
||||||
|
dependencies = [
|
||||||
|
"test-log-macros",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-log-macros"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "testing_logger"
|
name = "testing_logger"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -4364,13 +4385,14 @@ version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.0",
|
"base64 0.22.0",
|
||||||
"clap",
|
|
||||||
"http",
|
"http",
|
||||||
|
"insta",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"rust-netrc",
|
"rust-netrc",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"test-log",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -4492,6 +4514,8 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"uv-auth",
|
||||||
|
"uv-cache",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive", "env"], optional = true }
|
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
|
|
@ -21,3 +20,5 @@ urlencoding = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
wiremock = { workspace = true }
|
wiremock = { workspace = true }
|
||||||
|
insta = { version = "1.36.1" }
|
||||||
|
test-log = { version = "0.2.15", features = ["trace"], default-features = false }
|
||||||
|
|
|
||||||
184
crates/uv-auth/src/cache.rs
Normal file
184
crates/uv-auth/src/cache.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
|
use crate::credentials::Credentials;
|
||||||
|
use crate::NetLoc;
|
||||||
|
|
||||||
|
use tracing::trace;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
type CacheKey = (NetLoc, Option<String>);
|
||||||
|
|
||||||
|
pub struct CredentialsCache {
|
||||||
|
store: Mutex<HashMap<CacheKey, Arc<Credentials>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CheckResponse {
|
||||||
|
/// The given credentials should be used and are not present in the cache.
|
||||||
|
Uncached(Arc<Credentials>),
|
||||||
|
/// Credentials were found in the cache.
|
||||||
|
Cached(Arc<Credentials>),
|
||||||
|
// Credentials were not found in the cache and none were provided.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckResponse {
|
||||||
|
/// Retrieve the credentials, if any.
|
||||||
|
pub fn get(&self) -> Option<&Credentials> {
|
||||||
|
match self {
|
||||||
|
Self::Cached(credentials) => Some(credentials.as_ref()),
|
||||||
|
Self::Uncached(credentials) => Some(credentials.as_ref()),
|
||||||
|
Self::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there are credentials with a password.
|
||||||
|
pub fn is_authenticated(&self) -> bool {
|
||||||
|
self.get()
|
||||||
|
.is_some_and(|credentials| credentials.password().is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CredentialsCache {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CredentialsCache {
|
||||||
|
/// Create a new cache.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
store: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an owned cache key.
|
||||||
|
fn key(url: &Url, username: Option<String>) -> CacheKey {
|
||||||
|
(NetLoc::from(url), username)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the credentials that should be used for a URL, if any.
|
||||||
|
///
|
||||||
|
/// The [`Url`] is not checked for credentials. Existing credentials should be extracted and passed
|
||||||
|
/// separately.
|
||||||
|
///
|
||||||
|
/// If complete credentials are provided, they will be returned as [`CheckResponse::Existing`]
|
||||||
|
/// If the credentials are partial, i.e. missing a password, the cache will be checked
|
||||||
|
/// for a corresponding entry.
|
||||||
|
pub(crate) fn check(&self, url: &Url, credentials: Option<Credentials>) -> CheckResponse {
|
||||||
|
let store = self.store.lock().unwrap();
|
||||||
|
|
||||||
|
let credentials = credentials.map(Arc::new);
|
||||||
|
let key = CredentialsCache::key(
|
||||||
|
url,
|
||||||
|
credentials
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|credentials| credentials.username().map(str::to_string)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(credentials) = credentials {
|
||||||
|
if credentials.password().is_some() {
|
||||||
|
trace!("Existing credentials include password, skipping cache");
|
||||||
|
// No need to look-up, we have a password already
|
||||||
|
return CheckResponse::Uncached(credentials);
|
||||||
|
}
|
||||||
|
trace!("Existing credentials missing password, checking cache");
|
||||||
|
let existing = store.get(&key);
|
||||||
|
existing
|
||||||
|
.cloned()
|
||||||
|
.map(CheckResponse::Cached)
|
||||||
|
.inspect(|_| trace!("Found cached credentials."))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
trace!("No credentials in cache, using existing credentials");
|
||||||
|
CheckResponse::Uncached(credentials)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
trace!("No credentials on request, checking cache...");
|
||||||
|
store
|
||||||
|
.get(&key)
|
||||||
|
.cloned()
|
||||||
|
.map(CheckResponse::Cached)
|
||||||
|
.inspect(|_| trace!("Found cached credentials."))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
trace!("No credentials in cache.");
|
||||||
|
CheckResponse::None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the cache with the given credentials if none exist.
|
||||||
|
pub(crate) fn set_default(&self, url: &Url, credentials: Arc<Credentials>) {
|
||||||
|
// Do not cache empty credentials
|
||||||
|
if credentials.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert an entry for requests including the username
|
||||||
|
if let Some(username) = credentials.username() {
|
||||||
|
let key = CredentialsCache::key(url, Some(username.to_string()));
|
||||||
|
if !self.contains_key(&key) {
|
||||||
|
self.insert_entry(key, credentials.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert an entry for requests with no username
|
||||||
|
let key = CredentialsCache::key(url, None);
|
||||||
|
if !self.contains_key(&key) {
|
||||||
|
self.insert_entry(key, credentials.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the cache with the given credentials.
|
||||||
|
pub(crate) fn insert(&self, url: &Url, credentials: Arc<Credentials>) {
|
||||||
|
// Do not cache empty credentials
|
||||||
|
if credentials.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert an entry for requests including the username
|
||||||
|
if let Some(username) = credentials.username() {
|
||||||
|
self.insert_entry(
|
||||||
|
CredentialsCache::key(url, Some(username.to_string())),
|
||||||
|
credentials.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert an entry for requests with no username
|
||||||
|
self.insert_entry(CredentialsCache::key(url, None), credentials.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private interface to update a cache entry.
|
||||||
|
fn insert_entry(&self, key: (NetLoc, Option<String>), credentials: Arc<Credentials>) -> bool {
|
||||||
|
// Do not cache empty credentials
|
||||||
|
if credentials.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut store = self.store.lock().unwrap();
|
||||||
|
|
||||||
|
// Always replace existing entries if we have a password
|
||||||
|
if credentials.password().is_some() {
|
||||||
|
store.insert(key, credentials.clone());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we only have a username, add a new entry or replace an existing entry if it doesn't have a password
|
||||||
|
let existing = store.get(&key);
|
||||||
|
if existing.is_none()
|
||||||
|
|| existing.is_some_and(|credentials| credentials.password().is_none())
|
||||||
|
{
|
||||||
|
store.insert(key, credentials.clone());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if a key is in the cache.
|
||||||
|
fn contains_key(&self, key: &(NetLoc, Option<String>)) -> bool {
|
||||||
|
let store = self.store.lock().unwrap();
|
||||||
|
store.contains_key(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
277
crates/uv-auth/src/credentials.rs
Normal file
277
crates/uv-auth/src/credentials.rs
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use base64::read::DecoderReader;
|
||||||
|
use base64::write::EncoderWriter;
|
||||||
|
|
||||||
|
use netrc::Netrc;
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
|
use reqwest::Request;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::io::Write;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct Credentials {
|
||||||
|
/// The name of the user for authentication.
|
||||||
|
///
|
||||||
|
/// Unlike `reqwest`, empty usernames should be encoded as `None` instead of an empty string.
|
||||||
|
username: Option<String>,
|
||||||
|
/// The password to use for authentication.
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Credentials {
|
||||||
|
pub fn new(username: Option<String>, password: Option<String>) -> Self {
|
||||||
|
debug_assert!(
|
||||||
|
username.is_none()
|
||||||
|
|| username
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|username| !username.is_empty())
|
||||||
|
);
|
||||||
|
Self { username, password }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn username(&self) -> Option<&str> {
|
||||||
|
self.username.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&str> {
|
||||||
|
self.password.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.password.is_none() && self.username.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any.
|
||||||
|
///
|
||||||
|
/// If a username is provided, it must match the login in the netrc file or [`None`] is returned.
|
||||||
|
pub fn from_netrc(netrc: &Netrc, url: &Url, username: Option<&str>) -> Option<Self> {
|
||||||
|
let host = url.host_str()?;
|
||||||
|
let entry = netrc
|
||||||
|
.hosts
|
||||||
|
.get(host)
|
||||||
|
.or_else(|| netrc.hosts.get("default"))?;
|
||||||
|
|
||||||
|
// Ensure the username matches if provided
|
||||||
|
if username.is_some_and(|username| username != entry.login) {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Credentials {
|
||||||
|
username: Some(entry.login.clone()),
|
||||||
|
password: Some(entry.password.clone()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse [`Credentials`] from a URL, if any.
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if both [`Url::username`] and [`Url::password`] are not populated.
|
||||||
|
pub fn from_url(url: &Url) -> Option<Self> {
|
||||||
|
if url.username().is_empty() && url.password().is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self {
|
||||||
|
// Remove percent-encoding from URL credentials
|
||||||
|
// See <https://github.com/pypa/pip/blob/06d21db4ff1ab69665c22a88718a4ea9757ca293/src/pip/_internal/utils/misc.py#L497-L499>
|
||||||
|
username: if url.username().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
urlencoding::decode(url.username())
|
||||||
|
.expect("An encoded username should always decode")
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
password: url.password().map(|password| {
|
||||||
|
urlencoding::decode(password)
|
||||||
|
.expect("An encoded password should always decode")
|
||||||
|
.into_owned()
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse [`Credentials`] from an HTTP request, if any.
|
||||||
|
///
|
||||||
|
/// Only HTTP Basic Authentication is supported.
|
||||||
|
pub fn from_request(request: &Request) -> Option<Self> {
|
||||||
|
// First, attempt to retrieve the credentials from the URL
|
||||||
|
Self::from_url(request.url()).or(
|
||||||
|
// Then, attempt to pull the credentials from the headers
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::AUTHORIZATION)
|
||||||
|
.map(Self::from_header_value)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse [`Credentials`] from an authorization header, if any.
|
||||||
|
///
|
||||||
|
/// Only HTTP Basic Authentication is supported.
|
||||||
|
/// [`None`] will be returned if another authoriziation scheme is detected.
|
||||||
|
///
|
||||||
|
/// Panics if the authentication is not conformant to the HTTP Basic Authentication scheme:
|
||||||
|
/// - The contents must be base64 encoded
|
||||||
|
/// - There must be a `:` separator
|
||||||
|
pub(crate) fn from_header_value(header: &HeaderValue) -> Option<Self> {
|
||||||
|
let mut value = header.as_bytes().strip_prefix(b"Basic ")?;
|
||||||
|
let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
|
||||||
|
let mut buf = String::new();
|
||||||
|
decoder
|
||||||
|
.read_to_string(&mut buf)
|
||||||
|
.expect("HTTP Basic Authentication should be base64 encoded.");
|
||||||
|
let (username, password) = buf
|
||||||
|
.split_once(':')
|
||||||
|
.expect("HTTP Basic Authentication should include a `:` separator.");
|
||||||
|
let username = if username.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(username.to_string())
|
||||||
|
};
|
||||||
|
let password = if password.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(password.to_string())
|
||||||
|
};
|
||||||
|
Some(Self::new(username, password))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an HTTP Basic Authentication header for the credentials.
|
||||||
|
///
|
||||||
|
/// Panics if the username or password cannot be base64 encoded.
|
||||||
|
pub(crate) fn to_header_value(&self) -> HeaderValue {
|
||||||
|
// See: <https://github.com/seanmonstar/reqwest/blob/2c11ef000b151c2eebeed2c18a7b81042220c6b0/src/util.rs#L3>
|
||||||
|
let mut buf = b"Basic ".to_vec();
|
||||||
|
{
|
||||||
|
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
|
||||||
|
write!(encoder, "{}:", self.username().unwrap_or_default())
|
||||||
|
.expect("Write to base64 encoder should succeed");
|
||||||
|
if let Some(password) = self.password() {
|
||||||
|
write!(encoder, "{}", password).expect("Write to base64 encoder should succeed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
|
||||||
|
header.set_sensitive(true);
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach the credentials to the given request.
|
||||||
|
///
|
||||||
|
/// Any existing credentials will be overridden.
|
||||||
|
#[must_use]
|
||||||
|
pub fn authenticate(&self, mut request: reqwest::Request) -> reqwest::Request {
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
|
||||||
|
request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use insta::assert_debug_snapshot;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_no_credentials() {
|
||||||
|
let url = &Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
assert_eq!(Credentials::from_url(url), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_username_and_password() {
|
||||||
|
let url = &Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_username("user").unwrap();
|
||||||
|
auth_url.set_password(Some("password")).unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
assert_eq!(credentials.username(), Some("user"));
|
||||||
|
assert_eq!(credentials.password(), Some("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_no_username() {
|
||||||
|
let url = &Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_password(Some("password")).unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
assert_eq!(credentials.username(), None);
|
||||||
|
assert_eq!(credentials.password(), Some("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_url_no_password() {
|
||||||
|
let url = &Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_username("user").unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
assert_eq!(credentials.username(), Some("user"));
|
||||||
|
assert_eq!(credentials.password(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticated_request_from_url() {
|
||||||
|
let url = Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_username("user").unwrap();
|
||||||
|
auth_url.set_password(Some("password")).unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
|
||||||
|
let mut request = reqwest::Request::new(reqwest::Method::GET, url);
|
||||||
|
request = credentials.authenticate(request);
|
||||||
|
|
||||||
|
let mut header = request
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::AUTHORIZATION)
|
||||||
|
.expect("Authorization header should be set")
|
||||||
|
.clone();
|
||||||
|
header.set_sensitive(false);
|
||||||
|
|
||||||
|
assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###);
|
||||||
|
assert_eq!(Credentials::from_header_value(&header), Some(credentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticated_request_from_url_with_percent_encoded_user() {
|
||||||
|
let url = Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_username("user@domain").unwrap();
|
||||||
|
auth_url.set_password(Some("password")).unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
|
||||||
|
let mut request = reqwest::Request::new(reqwest::Method::GET, url);
|
||||||
|
request = credentials.authenticate(request);
|
||||||
|
|
||||||
|
let mut header = request
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::AUTHORIZATION)
|
||||||
|
.expect("Authorization header should be set")
|
||||||
|
.clone();
|
||||||
|
header.set_sensitive(false);
|
||||||
|
|
||||||
|
assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###);
|
||||||
|
assert_eq!(Credentials::from_header_value(&header), Some(credentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticated_request_from_url_with_percent_encoded_password() {
|
||||||
|
let url = Url::parse("https://example.com/simple/first/").unwrap();
|
||||||
|
let mut auth_url = url.clone();
|
||||||
|
auth_url.set_username("user").unwrap();
|
||||||
|
auth_url.set_password(Some("password==")).unwrap();
|
||||||
|
let credentials = Credentials::from_url(&auth_url).unwrap();
|
||||||
|
|
||||||
|
let mut request = reqwest::Request::new(reqwest::Method::GET, url);
|
||||||
|
request = credentials.authenticate(request);
|
||||||
|
|
||||||
|
let mut header = request
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::AUTHORIZATION)
|
||||||
|
.expect("Authorization header should be set")
|
||||||
|
.clone();
|
||||||
|
header.set_sensitive(false);
|
||||||
|
|
||||||
|
assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###);
|
||||||
|
assert_eq!(Credentials::from_header_value(&header), Some(credentials));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,88 +1,161 @@
|
||||||
use std::process::Command;
|
use std::{collections::HashSet, process::Command, sync::Mutex};
|
||||||
|
|
||||||
use thiserror::Error;
|
use tracing::{debug, instrument, warn};
|
||||||
use tracing::debug;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::store::{BasicAuthData, Credential};
|
use crate::credentials::Credentials;
|
||||||
|
|
||||||
/// Keyring provider to use for authentication
|
/// A backend for retrieving credentials from a keyring.
|
||||||
///
|
///
|
||||||
/// See <https://pip.pypa.io/en/stable/topics/authentication/#keyring-support>
|
/// See pip's implementation for reference
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
|
||||||
pub enum KeyringProvider {
|
|
||||||
/// Will not use keyring for authentication
|
|
||||||
#[default]
|
|
||||||
Disabled,
|
|
||||||
/// Will use keyring CLI command for authentication
|
|
||||||
Subprocess,
|
|
||||||
// /// Not yet implemented
|
|
||||||
// Auto,
|
|
||||||
// /// Not implemented yet. Maybe use <https://docs.rs/keyring/latest/keyring/> for this?
|
|
||||||
// Import,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Url is not valid Keyring target: {0}")]
|
|
||||||
NotKeyringTarget(String),
|
|
||||||
#[error(transparent)]
|
|
||||||
CliFailure(#[from] std::io::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
ParseFailed(#[from] std::string::FromUtf8Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get credentials from keyring for given url
|
|
||||||
///
|
|
||||||
/// See `pip`'s KeyringCLIProvider
|
|
||||||
/// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L102>
|
/// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L102>
|
||||||
pub fn get_keyring_subprocess_auth(url: &Url) -> Result<Option<Credential>, Error> {
|
#[derive(Debug)]
|
||||||
let host = url.host_str();
|
pub struct KeyringProvider {
|
||||||
if host.is_none() {
|
/// Tracks host and username pairs with no credentials to avoid repeated queries.
|
||||||
return Err(Error::NotKeyringTarget(
|
cache: Mutex<HashSet<(String, String)>>,
|
||||||
"Should only use keyring for urls with host".to_string(),
|
backend: KeyringProviderBackend,
|
||||||
));
|
}
|
||||||
}
|
|
||||||
if url.password().is_some() {
|
|
||||||
return Err(Error::NotKeyringTarget(
|
|
||||||
"Url already contains password - keyring not required".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let username = match url.username() {
|
|
||||||
u if !u.is_empty() => u,
|
|
||||||
// this is the username keyring.get_credentials returns as username for GCP registry
|
|
||||||
_ => "oauth2accesstoken",
|
|
||||||
};
|
|
||||||
debug!(
|
|
||||||
"Running `keyring get` for `{}` with username `{}`",
|
|
||||||
url.to_string(),
|
|
||||||
username
|
|
||||||
);
|
|
||||||
let output = match Command::new("keyring")
|
|
||||||
.arg("get")
|
|
||||||
.arg(url.to_string())
|
|
||||||
.arg(username)
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
Ok(output) if output.status.success() => Ok(Some(
|
|
||||||
String::from_utf8(output.stdout)
|
|
||||||
.map_err(Error::ParseFailed)?
|
|
||||||
.trim_end()
|
|
||||||
.to_owned(),
|
|
||||||
)),
|
|
||||||
Ok(_) => Ok(None),
|
|
||||||
Err(e) => Err(Error::CliFailure(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
output.map(|password| {
|
#[derive(Debug)]
|
||||||
password.map(|password| {
|
pub enum KeyringProviderBackend {
|
||||||
Credential::Basic(BasicAuthData {
|
/// Use the `keyring` command to fetch credentials.
|
||||||
username: username.to_string(),
|
Subprocess,
|
||||||
password: Some(password),
|
#[cfg(test)]
|
||||||
})
|
Dummy(std::collections::HashMap<(String, &'static str), &'static str>),
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
impl KeyringProvider {
|
||||||
|
/// Create a new [`KeyringProvider::Subprocess`].
|
||||||
|
pub fn subprocess() -> Self {
|
||||||
|
Self {
|
||||||
|
cache: Mutex::new(HashSet::new()),
|
||||||
|
backend: KeyringProviderBackend::Subprocess,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch credentials for the given [`Url`] from the keyring.
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if no password was found for the username or if any errors
|
||||||
|
/// are encountered in the keyring backend.
|
||||||
|
pub(crate) fn fetch(&self, url: &Url, username: &str) -> Option<Credentials> {
|
||||||
|
// Validate the request
|
||||||
|
debug_assert!(
|
||||||
|
url.host_str().is_some(),
|
||||||
|
"Should only use keyring for urls with host"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
url.password().is_none(),
|
||||||
|
"Should only use keyring for urls without a password"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
!username.is_empty(),
|
||||||
|
"Should only use keyring with a username"
|
||||||
|
);
|
||||||
|
|
||||||
|
let host = url.host_str()?;
|
||||||
|
|
||||||
|
// Avoid expensive lookups by tracking previous attempts with no credentials.
|
||||||
|
// N.B. We cache missing credentials per host so no credentials are found for
|
||||||
|
// a host but would return credentials for some other URL in the same realm
|
||||||
|
// we may not find the credentials depending on which URL we see first.
|
||||||
|
// This behavior avoids adding ~80ms to every request when the subprocess keyring
|
||||||
|
// provider is being used, but makes assumptions about the typical keyring
|
||||||
|
// use-cases.
|
||||||
|
let mut cache = self.cache.lock().unwrap();
|
||||||
|
|
||||||
|
let key = (host.to_string(), username.to_string());
|
||||||
|
if cache.contains(&key) {
|
||||||
|
debug!(
|
||||||
|
"Skipping keyring lookup for {username} at {host}, already attempted and found no credentials."
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the full URL first
|
||||||
|
// <https://github.com/pypa/pip/blob/ae5fff36b0aad6e5e0037884927eaa29163c0611/src/pip/_internal/network/auth.py#L376C1-L379C14>
|
||||||
|
let mut password = match self.backend {
|
||||||
|
KeyringProviderBackend::Subprocess => self.fetch_subprocess(url.as_str(), username),
|
||||||
|
#[cfg(test)]
|
||||||
|
KeyringProviderBackend::Dummy(ref store) => {
|
||||||
|
self.fetch_dummy(store, url.as_str(), username)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// And fallback to a check for the host
|
||||||
|
if password.is_none() {
|
||||||
|
password = match self.backend {
|
||||||
|
KeyringProviderBackend::Subprocess => self.fetch_subprocess(host, username),
|
||||||
|
#[cfg(test)]
|
||||||
|
KeyringProviderBackend::Dummy(ref store) => self.fetch_dummy(store, host, username),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if password.is_none() {
|
||||||
|
cache.insert(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
password.map(|password| Credentials::new(Some(username.to_string()), Some(password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn fetch_subprocess(&self, service_name: &str, username: &str) -> Option<String> {
|
||||||
|
let output = Command::new("keyring")
|
||||||
|
.arg("get")
|
||||||
|
.arg(service_name)
|
||||||
|
.arg(username)
|
||||||
|
.output()
|
||||||
|
.inspect_err(|err| warn!("Failure running `keyring` command: {err}"))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
// On success, parse the newline terminated password
|
||||||
|
String::from_utf8(output.stdout)
|
||||||
|
.inspect_err(|err| warn!("Failed to parse response from `keyring` command: {err}"))
|
||||||
|
.ok()
|
||||||
|
.map(|password| password.trim_end().to_string())
|
||||||
|
} else {
|
||||||
|
// On failure, no password was available
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn fetch_dummy(
|
||||||
|
&self,
|
||||||
|
store: &std::collections::HashMap<(String, &'static str), &'static str>,
|
||||||
|
service_name: &str,
|
||||||
|
username: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
store
|
||||||
|
.get(&(service_name.to_string(), username))
|
||||||
|
.map(|password| password.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new provider with [`KeyringProviderBackend::Dummy`].
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn dummy<S: Into<String>, T: IntoIterator<Item = ((S, &'static str), &'static str)>>(
|
||||||
|
iter: T,
|
||||||
|
) -> Self {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cache: Mutex::new(HashSet::new()),
|
||||||
|
backend: KeyringProviderBackend::Dummy(HashMap::from_iter(
|
||||||
|
iter.into_iter()
|
||||||
|
.map(|((service, username), password)| ((service.into(), username), password)),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new provider with no credentials available.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cache: Mutex::new(HashSet::new()),
|
||||||
|
backend: KeyringProviderBackend::Dummy(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -90,20 +163,138 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hostless_url_should_err() {
|
fn fetch_url_no_host() {
|
||||||
let url = Url::parse("file:/etc/bin/").unwrap();
|
let url = Url::parse("file:/etc/bin/").unwrap();
|
||||||
let res = get_keyring_subprocess_auth(&url);
|
let keyring = KeyringProvider::empty();
|
||||||
assert!(res.is_err());
|
// Panics due to debug assertion; returns `None` in production
|
||||||
assert!(matches!(res.unwrap_err(),
|
let result = std::panic::catch_unwind(|| keyring.fetch(&url, "user"));
|
||||||
Error::NotKeyringTarget(s) if s == "Should only use keyring for urls with host"));
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn passworded_url_should_err() {
|
fn fetch_url_with_password() {
|
||||||
let url = Url::parse("https://u:p@example.com").unwrap();
|
let url = Url::parse("https://user:password@example.com").unwrap();
|
||||||
let res = get_keyring_subprocess_auth(&url);
|
let keyring = KeyringProvider::empty();
|
||||||
assert!(res.is_err());
|
// Panics due to debug assertion; returns `None` in production
|
||||||
assert!(matches!(res.unwrap_err(),
|
let result = std::panic::catch_unwind(|| keyring.fetch(&url, url.username()));
|
||||||
Error::NotKeyringTarget(s) if s == "Url already contains password - keyring not required"));
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_with_no_username() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::empty();
|
||||||
|
// Panics due to debug assertion; returns `None` in production
|
||||||
|
let result = std::panic::catch_unwind(|| keyring.fetch(&url, url.username()));
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_no_auth() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::empty();
|
||||||
|
let credentials = keyring.fetch(&url, "user");
|
||||||
|
assert!(credentials.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
||||||
|
assert_eq!(
|
||||||
|
keyring.fetch(&url, "user"),
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keyring.fetch(&url.join("test").unwrap(), "user"),
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_no_match() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::dummy([(("other.com", "user"), "password")]);
|
||||||
|
let credentials = keyring.fetch(&url, "user");
|
||||||
|
assert_eq!(credentials, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_prefers_url_to_host() {
|
||||||
|
let url = Url::parse("https://example.com/").unwrap();
|
||||||
|
let keyring = KeyringProvider::dummy([
|
||||||
|
((url.join("foo").unwrap().as_str(), "user"), "password"),
|
||||||
|
((url.host_str().unwrap(), "user"), "other-password"),
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
keyring.fetch(&url.join("foo").unwrap(), "user"),
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keyring.fetch(&url, "user"),
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("other-password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
keyring.fetch(&url.join("bar").unwrap(), "user"),
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("other-password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 are not scoped to a realm.
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_caches_based_on_host() {
|
||||||
|
let url = Url::parse("https://example.com/").unwrap();
|
||||||
|
let keyring =
|
||||||
|
KeyringProvider::dummy([((url.join("foo").unwrap().as_str(), "user"), "password")]);
|
||||||
|
|
||||||
|
// If we attempt an unmatching URL first...
|
||||||
|
assert_eq!(keyring.fetch(&url.join("bar").unwrap(), "user"), None);
|
||||||
|
|
||||||
|
// ... we will cache the missing credentials on subsequent attempts
|
||||||
|
assert_eq!(keyring.fetch(&url.join("foo").unwrap(), "user"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_username() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]);
|
||||||
|
let credentials = keyring.fetch(&url, "user");
|
||||||
|
assert_eq!(
|
||||||
|
credentials,
|
||||||
|
Some(Credentials::new(
|
||||||
|
Some("user".to_string()),
|
||||||
|
Some("password".to_string())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_url_username_no_match() {
|
||||||
|
let url = Url::parse("https://example.com").unwrap();
|
||||||
|
let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "foo"), "password")]);
|
||||||
|
let credentials = keyring.fetch(&url, "bar");
|
||||||
|
assert_eq!(credentials, None);
|
||||||
|
|
||||||
|
// Still fails if we have `foo` in the URL itself
|
||||||
|
let url = Url::parse("https://foo@example.com").unwrap();
|
||||||
|
let credentials = keyring.fetch(&url, "bar");
|
||||||
|
assert_eq!(credentials, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,19 @@
|
||||||
|
mod cache;
|
||||||
|
mod credentials;
|
||||||
mod keyring;
|
mod keyring;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
mod store;
|
mod netloc;
|
||||||
|
|
||||||
|
use cache::CredentialsCache;
|
||||||
|
|
||||||
pub use keyring::KeyringProvider;
|
pub use keyring::KeyringProvider;
|
||||||
pub use middleware::AuthMiddleware;
|
pub use middleware::AuthMiddleware;
|
||||||
|
use netloc::NetLoc;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
pub use store::AuthenticationStore;
|
|
||||||
|
|
||||||
use url::Url;
|
// TODO(zanieb): Consider passing a cache explicitly throughout
|
||||||
|
|
||||||
// TODO(zanieb): Consider passing a store explicitly throughout
|
/// Global authentication cache for a uv invocation
|
||||||
|
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc7235#section-2.2>
|
/// This is used to share credentials across uv clients.
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc7230#section-5.5>
|
pub(crate) static CREDENTIALS_CACHE: Lazy<CredentialsCache> = Lazy::new(CredentialsCache::default);
|
||||||
//
|
|
||||||
// The "scheme" and "authority" components must match to retain authentication
|
|
||||||
// The "authority", is composed of the host and port.
|
|
||||||
//
|
|
||||||
// The scheme must always be an exact match.
|
|
||||||
// Note some clients such as Python's `requests` library allow an upgrade
|
|
||||||
// from `http` to `https` but this is not spec-compliant.
|
|
||||||
// <https://github.com/pypa/pip/blob/75f54cae9271179b8cc80435f92336c97e349f9d/src/pip/_vendor/requests/sessions.py#L133-L136>
|
|
||||||
//
|
|
||||||
// The host must always be an exact match.
|
|
||||||
//
|
|
||||||
// The port is only allowed to differ if it matches the "default port" for the scheme.
|
|
||||||
// However, `url` (and therefore `reqwest`) sets the `port` to `None` if it matches the default port
|
|
||||||
// so we do not need any special handling here.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
struct NetLoc {
|
|
||||||
scheme: String,
|
|
||||||
host: Option<String>,
|
|
||||||
port: Option<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Url> for NetLoc {
|
|
||||||
fn from(url: &Url) -> Self {
|
|
||||||
Self {
|
|
||||||
scheme: url.scheme().to_string(),
|
|
||||||
host: url.host_str().map(str::to_string),
|
|
||||||
port: url.port(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use url::{ParseError, Url};
|
|
||||||
|
|
||||||
use crate::NetLoc;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_should_retain_auth() -> Result<(), ParseError> {
|
|
||||||
// Exact match (https)
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exact match (with port)
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:1234")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exact match (http)
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("http://example.com")?),
|
|
||||||
NetLoc::from(&Url::parse("http://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Okay, path differs
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("http://example.com/foo")?),
|
|
||||||
NetLoc::from(&Url::parse("http://example.com/bar")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Okay, default port differs (https)
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:443")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Okay, default port differs (http)
|
|
||||||
assert_eq!(
|
|
||||||
NetLoc::from(&Url::parse("http://example.com:80")?),
|
|
||||||
NetLoc::from(&Url::parse("http://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched scheme
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?),
|
|
||||||
NetLoc::from(&Url::parse("http://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched scheme, we explicitly do not allow upgrade to https
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("http://example.com")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched host
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://foo.com")?),
|
|
||||||
NetLoc::from(&Url::parse("https://bar.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched port
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched port, with one as default for scheme
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:443")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
|
||||||
);
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:443")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mismatched port, with default for a different scheme
|
|
||||||
assert_ne!(
|
|
||||||
NetLoc::from(&Url::parse("https://example.com:80")?),
|
|
||||||
NetLoc::from(&Url::parse("https://example.com")?)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,69 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use http::Extensions;
|
use http::Extensions;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use netrc::Netrc;
|
use netrc::Netrc;
|
||||||
use reqwest::{header::HeaderValue, Request, Response};
|
use reqwest::{Request, Response};
|
||||||
use reqwest_middleware::{Middleware, Next};
|
use reqwest_middleware::{Middleware, Next};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
keyring::{get_keyring_subprocess_auth, KeyringProvider},
|
cache::CheckResponse, credentials::Credentials, CredentialsCache, KeyringProvider,
|
||||||
store::Credential,
|
CREDENTIALS_CACHE,
|
||||||
GLOBAL_AUTH_STORE,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A middleware that adds basic authentication to requests based on the netrc file and the keyring.
|
/// A middleware that adds basic authentication to requests based on the netrc file and the keyring.
|
||||||
///
|
///
|
||||||
/// Netrc support Based on: <https://github.com/gribouille/netrc>.
|
/// Netrc support Based on: <https://github.com/gribouille/netrc>.
|
||||||
pub struct AuthMiddleware {
|
pub struct AuthMiddleware {
|
||||||
nrc: Option<Netrc>,
|
netrc: Option<Netrc>,
|
||||||
keyring_provider: KeyringProvider,
|
keyring: Option<KeyringProvider>,
|
||||||
|
cache: Option<CredentialsCache>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthMiddleware {
|
impl AuthMiddleware {
|
||||||
pub fn new(keyring_provider: KeyringProvider) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
nrc: Netrc::new().ok(),
|
netrc: Netrc::new().ok(),
|
||||||
keyring_provider,
|
keyring: None,
|
||||||
|
cache: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_netrc_file(file: &Path, keyring_provider: KeyringProvider) -> Self {
|
/// Configure the [`Netrc`] credential file to use.
|
||||||
Self {
|
///
|
||||||
nrc: Netrc::from_file(file).ok(),
|
/// `None` disables authentication via netrc.
|
||||||
keyring_provider,
|
#[must_use]
|
||||||
}
|
pub fn with_netrc(mut self, netrc: Option<Netrc>) -> Self {
|
||||||
|
self.netrc = netrc;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure the [`KeyringProvider`] to use.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_keyring(mut self, keyring: Option<KeyringProvider>) -> Self {
|
||||||
|
self.keyring = keyring;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure the [`CredentialsCache`] to use.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_cache(mut self, cache: CredentialsCache) -> Self {
|
||||||
|
self.cache = Some(cache);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the configured authentication store.
|
||||||
|
///
|
||||||
|
/// If not set, the global store is used.
|
||||||
|
fn cache(&self) -> &CredentialsCache {
|
||||||
|
self.cache.as_ref().unwrap_or(&CREDENTIALS_CACHE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AuthMiddleware {
|
||||||
|
fn default() -> Self {
|
||||||
|
AuthMiddleware::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,145 +71,516 @@ impl AuthMiddleware {
|
||||||
impl Middleware for AuthMiddleware {
|
impl Middleware for AuthMiddleware {
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
mut req: Request,
|
mut request: Request,
|
||||||
_extensions: &mut Extensions,
|
extensions: &mut Extensions,
|
||||||
next: Next<'_>,
|
next: Next<'_>,
|
||||||
) -> reqwest_middleware::Result<Response> {
|
) -> reqwest_middleware::Result<Response> {
|
||||||
let url = req.url().clone();
|
// Check for credentials attached to (1) the request itself
|
||||||
|
let credentials = Credentials::from_request(&request);
|
||||||
|
// In the middleware, existing credentials are already moved from the URL
|
||||||
|
// to the headers so for display purposes we restore some information
|
||||||
|
let url = if tracing::enabled!(tracing::Level::DEBUG) {
|
||||||
|
let mut url = request.url().clone();
|
||||||
|
if let Some(username) = credentials
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|credentials| credentials.username())
|
||||||
|
{
|
||||||
|
let _ = url.set_username(username);
|
||||||
|
};
|
||||||
|
if credentials
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|credentials| credentials.password())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
let _ = url.set_password(Some("****"));
|
||||||
|
};
|
||||||
|
url.to_string()
|
||||||
|
} else {
|
||||||
|
request.url().to_string()
|
||||||
|
};
|
||||||
|
trace!("Handling request for {url}");
|
||||||
|
|
||||||
// If the request already has an authorization header, we don't need to do anything.
|
// Then check for credentials in (2) the cache
|
||||||
// This gives in-URL credentials precedence over the netrc file.
|
let credentials = self.cache().check(request.url(), credentials);
|
||||||
if req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
|
||||||
debug!("Request already has an authorization header: {url}");
|
|
||||||
return next.run(req, _extensions).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try auth strategies in order of precedence:
|
// Track credentials that we might want to insert into the cache
|
||||||
if let Some(stored_auth) = GLOBAL_AUTH_STORE.get(&url) {
|
let mut new_credentials = None;
|
||||||
// If we've already seen this URL, we can use the stored credentials
|
|
||||||
if let Some(auth) = stored_auth {
|
// If already authenticated (including a password), don't query other services
|
||||||
debug!("Adding authentication to already-seen URL: {url}");
|
if credentials.is_authenticated() {
|
||||||
req.headers_mut().insert(
|
match credentials {
|
||||||
reqwest::header::AUTHORIZATION,
|
// If we get credentials from the cache, update the request
|
||||||
basic_auth(auth.username(), auth.password()),
|
CheckResponse::Cached(credentials) => request = credentials.authenticate(request),
|
||||||
);
|
// If we get credentials from the request, we should update the cache
|
||||||
} else {
|
// but don't need to update the request
|
||||||
debug!("No credentials found for already-seen URL: {url}");
|
CheckResponse::Uncached(credentials) => new_credentials = Some(credentials.clone()),
|
||||||
|
CheckResponse::None => unreachable!("No credentials cannot be authenticated"),
|
||||||
}
|
}
|
||||||
} else if let Some(auth) = self.nrc.as_ref().and_then(|nrc| {
|
// Otherwise, look for complete credentials in:
|
||||||
// If we find a matching entry in the netrc file, we can use it
|
// (3) The netrc file
|
||||||
url.host_str()
|
} else if let Some(credentials) = self.netrc.as_ref().and_then(|netrc| {
|
||||||
.and_then(|host| nrc.hosts.get(host).or_else(|| nrc.hosts.get("default")))
|
trace!("Checking netrc for credentials for {url}");
|
||||||
|
Credentials::from_netrc(
|
||||||
|
netrc,
|
||||||
|
request.url(),
|
||||||
|
credentials
|
||||||
|
.get()
|
||||||
|
.and_then(|credentials| credentials.username()),
|
||||||
|
)
|
||||||
}) {
|
}) {
|
||||||
let auth = Credential::from(auth.to_owned());
|
debug!("Found credentials in netrc file for {url}");
|
||||||
req.headers_mut().insert(
|
request = credentials.authenticate(request);
|
||||||
reqwest::header::AUTHORIZATION,
|
new_credentials = Some(Arc::new(credentials));
|
||||||
basic_auth(auth.username(), auth.password()),
|
// (4) The keyring
|
||||||
);
|
// N.B. The keyring provider performs lookups for the exact URL then
|
||||||
GLOBAL_AUTH_STORE.set(&url, Some(auth));
|
// falls back to the host, but we cache the result per host so if a keyring
|
||||||
} else if matches!(self.keyring_provider, KeyringProvider::Subprocess) {
|
// implementation returns different credentials for different URLs in the
|
||||||
// If we have keyring support enabled, we check there as well
|
// same realm we will use the wrong credentials.
|
||||||
match get_keyring_subprocess_auth(&url) {
|
} else if let Some(credentials) = self.keyring.as_ref().and_then(|keyring| {
|
||||||
Ok(Some(auth)) => {
|
if let Some(username) = credentials
|
||||||
req.headers_mut().insert(
|
.get()
|
||||||
reqwest::header::AUTHORIZATION,
|
.and_then(|credentials| credentials.username())
|
||||||
basic_auth(auth.username(), auth.password()),
|
{
|
||||||
);
|
debug!("Checking keyring for credentials for {url}");
|
||||||
GLOBAL_AUTH_STORE.set(&url, Some(auth));
|
keyring.fetch(request.url(), username)
|
||||||
}
|
} else {
|
||||||
Ok(None) => {
|
trace!("Skipping keyring lookup for {url} with no username");
|
||||||
debug!("No keyring credentials found for {url}");
|
None
|
||||||
}
|
}
|
||||||
Err(e) => {
|
}) {
|
||||||
warn!("Failed to get keyring credentials for {url}: {e}");
|
debug!("Found credentials in keyring for {url}");
|
||||||
|
request = credentials.authenticate(request);
|
||||||
|
new_credentials = Some(Arc::new(credentials));
|
||||||
|
// No additional credentials were found
|
||||||
|
} else {
|
||||||
|
match credentials {
|
||||||
|
CheckResponse::Cached(credentials) => request = credentials.authenticate(request),
|
||||||
|
CheckResponse::Uncached(credentials) => new_credentials = Some(credentials.clone()),
|
||||||
|
CheckResponse::None => {
|
||||||
|
debug!("No credentials found for {url}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still don't have any credentials, we save the URL so we don't have to check netrc or keyring again
|
if let Some(credentials) = new_credentials {
|
||||||
if !req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
let url = request.url().clone();
|
||||||
debug!("No credentials found for: {url}");
|
|
||||||
GLOBAL_AUTH_STORE.set(&url, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
next.run(req, _extensions).await
|
// Update the default credentials eagerly since requests are made concurrently
|
||||||
}
|
// and we want to avoid expensive credential lookups
|
||||||
}
|
self.cache().set_default(&url, credentials.clone());
|
||||||
|
|
||||||
/// Create a `HeaderValue` for basic authentication.
|
let result = next.run(request, extensions).await;
|
||||||
///
|
|
||||||
/// Source: <https://github.com/seanmonstar/reqwest/blob/2c11ef000b151c2eebeed2c18a7b81042220c6b0/src/util.rs#L3>
|
|
||||||
fn basic_auth<U, P>(username: U, password: Option<P>) -> HeaderValue
|
|
||||||
where
|
|
||||||
U: std::fmt::Display,
|
|
||||||
P: std::fmt::Display,
|
|
||||||
{
|
|
||||||
use base64::prelude::BASE64_STANDARD;
|
|
||||||
use base64::write::EncoderWriter;
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
let mut buf = b"Basic ".to_vec();
|
// Only update the cache with new credentials on a successful request
|
||||||
{
|
if result
|
||||||
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
|
.as_ref()
|
||||||
let _ = write!(encoder, "{}:", username);
|
.is_ok_and(|response| response.error_for_status_ref().is_ok())
|
||||||
if let Some(password) = password {
|
{
|
||||||
let _ = write!(encoder, "{}", password);
|
trace!("Updating cached credentials for {url}");
|
||||||
|
self.cache().insert(&url, credentials)
|
||||||
|
};
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
next.run(request, extensions).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
|
|
||||||
header.set_sensitive(true);
|
|
||||||
header
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use reqwest_middleware::ClientBuilder;
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use wiremock::matchers::{basic_auth, method, path};
|
use test_log::test;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
use wiremock::matchers::{basic_auth, method};
|
||||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const NETRC: &str = r#"default login myuser password mypassword"#;
|
type Error = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
#[tokio::test]
|
async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
|
||||||
async fn test_init() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let server = MockServer::start().await;
|
let server = MockServer::start().await;
|
||||||
|
|
||||||
Mock::given(method("GET"))
|
Mock::given(method("GET"))
|
||||||
.and(path("/hello"))
|
.and(basic_auth(username, password))
|
||||||
.and(basic_auth("myuser", "mypassword"))
|
|
||||||
.respond_with(ResponseTemplate::new(200))
|
.respond_with(ResponseTemplate::new(200))
|
||||||
.mount(&server)
|
.mount(&server)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let status = ClientBuilder::new(Client::builder().build()?)
|
Mock::given(method("GET"))
|
||||||
.build()
|
.respond_with(ResponseTemplate::new(401))
|
||||||
.get(format!("{}/hello", &server.uri()))
|
.mount(&server)
|
||||||
.send()
|
.await;
|
||||||
.await?
|
|
||||||
.status();
|
|
||||||
|
|
||||||
assert_eq!(status, 404);
|
server
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_client_builder() -> reqwest_middleware::ClientBuilder {
|
||||||
|
reqwest_middleware::ClientBuilder::new(
|
||||||
|
Client::builder()
|
||||||
|
.build()
|
||||||
|
.expect("Reqwest client should build"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_no_credentials() -> Result<(), Error> {
|
||||||
|
let server = start_test_server("user", "password").await;
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.get(format!("{}/foo", server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status(),
|
||||||
|
401
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.get(format!("{}/bar", server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status(),
|
||||||
|
401
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_credentials_in_url() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let base_url = Url::parse(&server.uri())?;
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some(password)).unwrap();
|
||||||
|
assert_eq!(client.get(url).send().await?.status(), 200);
|
||||||
|
|
||||||
|
// Works for a URL without credentials now
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not require credentials"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.get(format!("{}/foo", server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests can be to different paths in the same realm"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some("invalid")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials in the URL should take precedence and fail"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_credentials_in_url_username_only() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "";
|
||||||
|
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let base_url = Url::parse(&server.uri())?;
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(None).unwrap();
|
||||||
|
assert_eq!(client.get(url).send().await?.status(), 200);
|
||||||
|
|
||||||
|
// Works for a URL without credentials now
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not require credentials"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
client
|
||||||
|
.get(format!("{}/foo", server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests can be to different paths in the same realm"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some("invalid")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials in the URL should take precedence and fail"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not use the invalid credentials"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_netrc_file_default_host() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
|
||||||
let mut netrc_file = NamedTempFile::new()?;
|
let mut netrc_file = NamedTempFile::new()?;
|
||||||
writeln!(netrc_file, "{}", NETRC)?;
|
writeln!(netrc_file, "default login {username} password {password}")?;
|
||||||
|
|
||||||
let status = ClientBuilder::new(Client::builder().build()?)
|
let server = start_test_server(username, password).await;
|
||||||
.with(AuthMiddleware::from_netrc_file(
|
let client = test_client_builder()
|
||||||
netrc_file.path(),
|
.with(
|
||||||
KeyringProvider::Disabled,
|
AuthMiddleware::new()
|
||||||
))
|
.with_cache(CredentialsCache::new())
|
||||||
.build()
|
.with_netrc(Netrc::from_file(netrc_file.path()).ok()),
|
||||||
.get(format!("{}/hello", &server.uri()))
|
)
|
||||||
.send()
|
.build();
|
||||||
.await?
|
|
||||||
.status();
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Credentials should be pulled from the netrc file"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = Url::parse(&server.uri())?;
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some("invalid")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials in the URL should take precedence and fail"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not use the invalid credentials"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_netrc_file_matching_host() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
let base_url = Url::parse(&server.uri())?;
|
||||||
|
|
||||||
|
let mut netrc_file = NamedTempFile::new()?;
|
||||||
|
writeln!(
|
||||||
|
netrc_file,
|
||||||
|
r#"machine {} login {username} password {password}"#,
|
||||||
|
base_url.host_str().unwrap()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(
|
||||||
|
AuthMiddleware::new()
|
||||||
|
.with_cache(CredentialsCache::new())
|
||||||
|
.with_netrc(Some(
|
||||||
|
Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Credentials should be pulled from the netrc file"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some("invalid")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials in the URL should take precedence and fail"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not use the invalid credentials"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
|
||||||
|
let mut netrc_file = NamedTempFile::new()?;
|
||||||
|
writeln!(
|
||||||
|
netrc_file,
|
||||||
|
r#"machine example.com login {username} password {password}"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(
|
||||||
|
AuthMiddleware::new()
|
||||||
|
.with_cache(CredentialsCache::new())
|
||||||
|
.with_netrc(Some(
|
||||||
|
Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials should not be pulled from the netrc file due to host mistmatch"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = Url::parse(&server.uri())?;
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some(password)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Credentials in the URL should still work"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
let base_url = Url::parse(&server.uri())?;
|
||||||
|
|
||||||
|
let mut netrc_file = NamedTempFile::new()?;
|
||||||
|
writeln!(
|
||||||
|
netrc_file,
|
||||||
|
r#"machine {} login {username} password {password}"#,
|
||||||
|
base_url.host_str().unwrap()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(
|
||||||
|
AuthMiddleware::new()
|
||||||
|
.with_cache(CredentialsCache::new())
|
||||||
|
.with_netrc(Some(
|
||||||
|
Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username("other-user").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"The netrc password should not be used due to a username mismatch"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username("user").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"The netrc password should be used for a matching user"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test(tokio::test)]
|
||||||
|
async fn test_keyring() -> Result<(), Error> {
|
||||||
|
let username = "user";
|
||||||
|
let password = "password";
|
||||||
|
let server = start_test_server(username, password).await;
|
||||||
|
let base_url = Url::parse(&server.uri())?;
|
||||||
|
|
||||||
|
let client = test_client_builder()
|
||||||
|
.with(
|
||||||
|
AuthMiddleware::new()
|
||||||
|
.with_cache(CredentialsCache::new())
|
||||||
|
.with_keyring(Some(KeyringProvider::dummy([(
|
||||||
|
(base_url.host_str().unwrap(), username),
|
||||||
|
password,
|
||||||
|
)]))),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
client.get(server.uri()).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials are not pulled from the keyring without a username"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Credentials for the username should be pulled from the keyring"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
url.set_password(Some("invalid")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Password in the URL should take precedence and fail"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username(username).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url.clone()).send().await?.status(),
|
||||||
|
200,
|
||||||
|
"Subsequent requests should not use the invalid password"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut url = base_url.clone();
|
||||||
|
url.set_username("other_user").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
client.get(url).send().await?.status(),
|
||||||
|
401,
|
||||||
|
"Credentials are not pulled from the keyring when given another username"
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(status, 200);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
crates/uv-auth/src/netloc.rs
Normal file
125
crates/uv-auth/src/netloc.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Used to determine if authentication information should be retained on a new URL.
|
||||||
|
/// Based on the specification defined in RFC 7235 and 7230.
|
||||||
|
///
|
||||||
|
/// <https://datatracker.ietf.org/doc/html/rfc7235#section-2.2>
|
||||||
|
/// <https://datatracker.ietf.org/doc/html/rfc7230#section-5.5>
|
||||||
|
//
|
||||||
|
// The "scheme" and "authority" components must match to retain authentication
|
||||||
|
// The "authority", is composed of the host and port.
|
||||||
|
//
|
||||||
|
// The scheme must always be an exact match.
|
||||||
|
// Note some clients such as Python's `requests` library allow an upgrade
|
||||||
|
// from `http` to `https` but this is not spec-compliant.
|
||||||
|
// <https://github.com/pypa/pip/blob/75f54cae9271179b8cc80435f92336c97e349f9d/src/pip/_vendor/requests/sessions.py#L133-L136>
|
||||||
|
//
|
||||||
|
// The host must always be an exact match.
|
||||||
|
//
|
||||||
|
// The port is only allowed to differ if it matches the "default port" for the scheme.
|
||||||
|
// However, `url` (and therefore `reqwest`) sets the `port` to `None` if it matches the default port
|
||||||
|
// so we do not need any special handling here.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) struct NetLoc {
|
||||||
|
scheme: String,
|
||||||
|
host: Option<String>,
|
||||||
|
port: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Url> for NetLoc {
|
||||||
|
fn from(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
scheme: url.scheme().to_string(),
|
||||||
|
host: url.host_str().map(str::to_string),
|
||||||
|
port: url.port(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
use crate::NetLoc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_retain_auth() -> Result<(), ParseError> {
|
||||||
|
// Exact match (https)
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exact match (with port)
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:1234")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exact match (http)
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("http://example.com")?),
|
||||||
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Okay, path differs
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("http://example.com/foo")?),
|
||||||
|
NetLoc::from(&Url::parse("http://example.com/bar")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Okay, default port differs (https)
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:443")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Okay, default port differs (http)
|
||||||
|
assert_eq!(
|
||||||
|
NetLoc::from(&Url::parse("http://example.com:80")?),
|
||||||
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched scheme
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?),
|
||||||
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched scheme, we explicitly do not allow upgrade to https
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("http://example.com")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched host
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://foo.com")?),
|
||||||
|
NetLoc::from(&Url::parse("https://bar.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched port
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched port, with one as default for scheme
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:443")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:443")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mismatched port, with default for a different scheme
|
||||||
|
assert_ne!(
|
||||||
|
NetLoc::from(&Url::parse("https://example.com:80")?),
|
||||||
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
use std::{collections::HashMap, sync::Mutex};
|
|
||||||
|
|
||||||
use netrc::Authenticator;
|
|
||||||
use tracing::warn;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::NetLoc;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum Credential {
|
|
||||||
Basic(BasicAuthData),
|
|
||||||
UrlEncoded(UrlAuthData),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Credential {
|
|
||||||
pub fn username(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Credential::Basic(auth) => &auth.username,
|
|
||||||
Credential::UrlEncoded(auth) => &auth.username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn password(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Credential::Basic(auth) => auth.password.as_deref(),
|
|
||||||
Credential::UrlEncoded(auth) => auth.password.as_deref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Authenticator> for Credential {
|
|
||||||
fn from(auth: Authenticator) -> Self {
|
|
||||||
Credential::Basic(BasicAuthData {
|
|
||||||
username: auth.login,
|
|
||||||
password: Some(auth.password),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for URL encoded auth in User info
|
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1>
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct UrlAuthData {
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UrlAuthData {
|
|
||||||
pub fn apply_to_url(&self, mut url: Url) -> Url {
|
|
||||||
url.set_username(&self.username)
|
|
||||||
.unwrap_or_else(|()| warn!("Failed to set username"));
|
|
||||||
url.set_password(self.password.as_deref())
|
|
||||||
.unwrap_or_else(|()| warn!("Failed to set password"));
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpBasicAuth - Used for netrc and keyring auth
|
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc7617>
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct BasicAuthData {
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AuthenticationStore {
|
|
||||||
credentials: Mutex<HashMap<NetLoc, Option<Credential>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AuthenticationStore {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthenticationStore {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
credentials: Mutex::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, url: &Url) -> Option<Option<Credential>> {
|
|
||||||
let netloc = NetLoc::from(url);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store in-URL credentials for future use.
|
|
||||||
pub fn save_from_url(&self, url: &Url) {
|
|
||||||
let netloc = NetLoc::from(url);
|
|
||||||
let mut credentials = self.credentials.lock().unwrap();
|
|
||||||
if url.username().is_empty() {
|
|
||||||
// No credentials to save
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let auth = UrlAuthData {
|
|
||||||
// Using the encoded username can break authentication when `@` is converted to `%40`
|
|
||||||
// so we decode it for storage; RFC7617 does not explicitly say that authentication should
|
|
||||||
// not be percent-encoded, but the omission of percent-encoding from all encoding discussion
|
|
||||||
// indicates that it probably should not be done.
|
|
||||||
username: urlencoding::decode(url.username())
|
|
||||||
.expect("An encoded username should always decode")
|
|
||||||
.into_owned(),
|
|
||||||
password: url.password().map(str::to_string),
|
|
||||||
};
|
|
||||||
credentials.insert(netloc, Some(Credential::UrlEncoded(auth)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
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://example2.com/simple/first/").unwrap();
|
|
||||||
let not_found_first_url = Url::parse("https://example3.com/simple/first/").unwrap();
|
|
||||||
|
|
||||||
store.set(
|
|
||||||
&found_first_url,
|
|
||||||
Some(Credential::Basic(BasicAuthData {
|
|
||||||
username: "u".to_string(),
|
|
||||||
password: Some("p".to_string()),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
store.set(¬_found_first_url, None);
|
|
||||||
|
|
||||||
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 = 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 = 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 store_save_from_url() {
|
|
||||||
let store = AuthenticationStore::new();
|
|
||||||
let url = Url::parse("https://u:p@example.com/simple/").unwrap();
|
|
||||||
|
|
||||||
store.save_from_url(&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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,8 @@ use std::fmt::Debug;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use uv_auth::{AuthMiddleware, KeyringProvider};
|
use uv_auth::AuthMiddleware;
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_version::version;
|
use uv_version::version;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
@ -21,7 +22,7 @@ use crate::Connectivity;
|
||||||
/// A builder for an [`BaseClient`].
|
/// A builder for an [`BaseClient`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BaseClientBuilder<'a> {
|
pub struct BaseClientBuilder<'a> {
|
||||||
keyring_provider: KeyringProvider,
|
keyring: KeyringProviderType,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
retries: u32,
|
retries: u32,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -39,7 +40,7 @@ impl Default for BaseClientBuilder<'_> {
|
||||||
impl BaseClientBuilder<'_> {
|
impl BaseClientBuilder<'_> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
keyring_provider: KeyringProvider::default(),
|
keyring: KeyringProviderType::default(),
|
||||||
native_tls: false,
|
native_tls: false,
|
||||||
connectivity: Connectivity::Online,
|
connectivity: Connectivity::Online,
|
||||||
retries: 3,
|
retries: 3,
|
||||||
|
|
@ -52,8 +53,8 @@ impl BaseClientBuilder<'_> {
|
||||||
|
|
||||||
impl<'a> BaseClientBuilder<'a> {
|
impl<'a> BaseClientBuilder<'a> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
pub fn keyring(mut self, keyring_type: KeyringProviderType) -> Self {
|
||||||
self.keyring_provider = keyring_provider;
|
self.keyring = keyring_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +170,8 @@ impl<'a> BaseClientBuilder<'a> {
|
||||||
let client = client.with(retry_strategy);
|
let client = client.with(retry_strategy);
|
||||||
|
|
||||||
// Initialize the authentication middleware to set headers.
|
// Initialize the authentication middleware to set headers.
|
||||||
let client = client.with(AuthMiddleware::new(self.keyring_provider));
|
let client =
|
||||||
|
client.with(AuthMiddleware::new().with_keyring(self.keyring.to_provider()));
|
||||||
|
|
||||||
client.build()
|
client.build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ use pep440_rs::Version;
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
use platform_tags::Platform;
|
use platform_tags::Platform;
|
||||||
use pypi_types::{Metadata23, SimpleJson};
|
use pypi_types::{Metadata23, SimpleJson};
|
||||||
use uv_auth::KeyringProvider;
|
|
||||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||||
use uv_configuration::IndexStrategy;
|
use uv_configuration::IndexStrategy;
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::base_client::{BaseClient, BaseClientBuilder};
|
use crate::base_client::{BaseClient, BaseClientBuilder};
|
||||||
|
|
@ -37,7 +37,7 @@ use crate::{CachedClient, CachedClientError, Error, ErrorKind};
|
||||||
pub struct RegistryClientBuilder<'a> {
|
pub struct RegistryClientBuilder<'a> {
|
||||||
index_urls: IndexUrls,
|
index_urls: IndexUrls,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring: KeyringProviderType,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
retries: u32,
|
retries: u32,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -52,7 +52,7 @@ impl RegistryClientBuilder<'_> {
|
||||||
Self {
|
Self {
|
||||||
index_urls: IndexUrls::default(),
|
index_urls: IndexUrls::default(),
|
||||||
index_strategy: IndexStrategy::default(),
|
index_strategy: IndexStrategy::default(),
|
||||||
keyring_provider: KeyringProvider::default(),
|
keyring: KeyringProviderType::default(),
|
||||||
native_tls: false,
|
native_tls: false,
|
||||||
cache,
|
cache,
|
||||||
connectivity: Connectivity::Online,
|
connectivity: Connectivity::Online,
|
||||||
|
|
@ -78,8 +78,8 @@ impl<'a> RegistryClientBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
pub fn keyring(mut self, keyring_type: KeyringProviderType) -> Self {
|
||||||
self.keyring_provider = keyring_provider;
|
self.keyring = keyring_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ impl<'a> RegistryClientBuilder<'a> {
|
||||||
.retries(self.retries)
|
.retries(self.retries)
|
||||||
.connectivity(self.connectivity)
|
.connectivity(self.connectivity)
|
||||||
.native_tls(self.native_tls)
|
.native_tls(self.native_tls)
|
||||||
.keyring_provider(self.keyring_provider)
|
.keyring(self.keyring)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let timeout = client.timeout();
|
let timeout = client.timeout();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
|
uv-cache = { workspace = true }
|
||||||
|
uv-auth = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
|
||||||
26
crates/uv-configuration/src/authentication.rs
Normal file
26
crates/uv-configuration/src/authentication.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use uv_auth::{self, KeyringProvider};
|
||||||
|
|
||||||
|
/// Keyring provider type to use for credential lookup.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
pub enum KeyringProviderType {
|
||||||
|
/// Do not use keyring for credential lookup.
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
/// Use the `keyring` command for credential lookup.
|
||||||
|
Subprocess,
|
||||||
|
// /// Not yet implemented
|
||||||
|
// Auto,
|
||||||
|
// /// Not implemented yet. Maybe use <https://docs.rs/keyring/latest/keyring/> for this?
|
||||||
|
// Import,
|
||||||
|
}
|
||||||
|
// See <https://pip.pypa.io/en/stable/topics/authentication/#keyring-support> for details.
|
||||||
|
|
||||||
|
impl KeyringProviderType {
|
||||||
|
pub fn to_provider(&self) -> Option<uv_auth::KeyringProvider> {
|
||||||
|
match self {
|
||||||
|
Self::Disabled => None,
|
||||||
|
Self::Subprocess => Some(KeyringProvider::subprocess()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub use authentication::*;
|
||||||
pub use build_options::*;
|
pub use build_options::*;
|
||||||
pub use config_settings::*;
|
pub use config_settings::*;
|
||||||
pub use constraints::*;
|
pub use constraints::*;
|
||||||
|
|
@ -5,6 +6,7 @@ pub use name_specifiers::*;
|
||||||
pub use overrides::*;
|
pub use overrides::*;
|
||||||
pub use package_options::*;
|
pub use package_options::*;
|
||||||
|
|
||||||
|
mod authentication;
|
||||||
mod build_options;
|
mod build_options;
|
||||||
mod config_settings;
|
mod config_settings;
|
||||||
mod constraints;
|
mod constraints;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pep508_rs = { workspace = true }
|
||||||
platform-tags = { workspace = true }
|
platform-tags = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
pypi-types = { workspace = true }
|
||||||
requirements-txt = { workspace = true, features = ["http"] }
|
requirements-txt = { workspace = true, features = ["http"] }
|
||||||
uv-auth = { workspace = true, features = ["clap"] }
|
uv-auth = { workspace = true }
|
||||||
uv-cache = { workspace = true, features = ["clap"] }
|
uv-cache = { workspace = true, features = ["clap"] }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-dispatch = { workspace = true }
|
uv-dispatch = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ use anyhow::Result;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||||
use uv_auth::KeyringProvider;
|
|
||||||
use uv_cache::CacheArgs;
|
use uv_cache::CacheArgs;
|
||||||
use uv_configuration::IndexStrategy;
|
use uv_configuration::{
|
||||||
use uv_configuration::{ConfigSettingEntry, PackageNameSpecifier};
|
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
|
||||||
|
};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
||||||
use uv_toolchain::PythonVersion;
|
use uv_toolchain::PythonVersion;
|
||||||
|
|
@ -362,7 +362,7 @@ pub(crate) struct PipCompileArgs {
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
pub(crate) keyring_provider: KeyringProvider,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
||||||
/// Locations to search for candidate distributions, beyond those found in the indexes.
|
/// Locations to search for candidate distributions, beyond those found in the indexes.
|
||||||
///
|
///
|
||||||
|
|
@ -572,7 +572,7 @@ pub(crate) struct PipSyncArgs {
|
||||||
/// Function's similar to `pip`'s `--keyring-provider subprocess` argument,
|
/// Function's similar to `pip`'s `--keyring-provider subprocess` argument,
|
||||||
/// `uv` will try to use `keyring` via CLI when this flag is used.
|
/// `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
pub(crate) keyring_provider: KeyringProvider,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
||||||
/// The Python interpreter into which packages should be installed.
|
/// The Python interpreter into which packages should be installed.
|
||||||
///
|
///
|
||||||
|
|
@ -845,7 +845,7 @@ pub(crate) struct PipInstallArgs {
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
pub(crate) keyring_provider: KeyringProvider,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
||||||
/// The Python interpreter into which packages should be installed.
|
/// The Python interpreter into which packages should be installed.
|
||||||
///
|
///
|
||||||
|
|
@ -994,7 +994,7 @@ pub(crate) struct PipUninstallArgs {
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
pub(crate) keyring_provider: KeyringProvider,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
||||||
/// Use the system Python to uninstall packages.
|
/// Use the system Python to uninstall packages.
|
||||||
///
|
///
|
||||||
|
|
@ -1290,7 +1290,7 @@ pub(crate) struct VenvArgs {
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
/// implemented `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
pub(crate) keyring_provider: KeyringProvider,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
||||||
/// Run offline, i.e., without accessing the network.
|
/// Run offline, i.e., without accessing the network.
|
||||||
#[arg(global = true, long)]
|
#[arg(global = true, long)]
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ use distribution_types::{IndexLocations, LocalEditable, LocalEditables, Verbatim
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, SetupPyStrategy,
|
ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, SetupPyStrategy,
|
||||||
Upgrade,
|
Upgrade,
|
||||||
|
|
@ -70,7 +70,7 @@ pub(crate) async fn pip_compile(
|
||||||
include_index_annotation: bool,
|
include_index_annotation: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: ConfigSettings,
|
config_settings: ConfigSettings,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -98,7 +98,7 @@ pub(crate) async fn pip_compile(
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.keyring_provider(keyring_provider);
|
.keyring(keyring_provider);
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let RequirementsSpecification {
|
let RequirementsSpecification {
|
||||||
|
|
@ -212,18 +212,13 @@ pub(crate) async fn pip_compile(
|
||||||
let index_locations =
|
let index_locations =
|
||||||
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
||||||
|
|
||||||
// Add all authenticated sources to the store.
|
|
||||||
for url in index_locations.urls() {
|
|
||||||
GLOBAL_AUTH_STORE.save_from_url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.index_strategy(index_strategy)
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring(keyring_provider)
|
||||||
.markers(&markers)
|
.markers(&markers)
|
||||||
.platform(interpreter.platform())
|
.platform(interpreter.platform())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ use pep508_rs::{MarkerEnvironment, Requirement};
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::{Metadata23, Yanked};
|
use pypi_types::{Metadata23, Yanked};
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{
|
use uv_client::{
|
||||||
BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder,
|
BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder,
|
||||||
};
|
};
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, Reinstall,
|
ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, Reinstall,
|
||||||
SetupPyStrategy, Upgrade,
|
SetupPyStrategy, Upgrade,
|
||||||
|
|
@ -63,7 +63,7 @@ pub(crate) async fn pip_install(
|
||||||
upgrade: Upgrade,
|
upgrade: Upgrade,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
reinstall: Reinstall,
|
reinstall: Reinstall,
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
compile: bool,
|
compile: bool,
|
||||||
|
|
@ -89,7 +89,7 @@ pub(crate) async fn pip_install(
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.keyring_provider(keyring_provider);
|
.keyring(keyring_provider);
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let RequirementsSpecification {
|
let RequirementsSpecification {
|
||||||
|
|
@ -203,18 +203,13 @@ pub(crate) async fn pip_install(
|
||||||
let index_locations =
|
let index_locations =
|
||||||
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
||||||
|
|
||||||
// Add all authenticated sources to the store.
|
|
||||||
for url in index_locations.urls() {
|
|
||||||
GLOBAL_AUTH_STORE.save_from_url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.index_strategy(index_strategy)
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring(keyring_provider)
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.platform(interpreter.platform())
|
.platform(interpreter.platform())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ use install_wheel_rs::linker::LinkMode;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Yanked;
|
use pypi_types::Yanked;
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
|
||||||
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache};
|
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache};
|
||||||
use uv_client::{
|
use uv_client::{
|
||||||
BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder,
|
BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder,
|
||||||
};
|
};
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
ConfigSettings, IndexStrategy, NoBinary, NoBuild, Reinstall, SetupPyStrategy,
|
ConfigSettings, IndexStrategy, NoBinary, NoBuild, Reinstall, SetupPyStrategy,
|
||||||
};
|
};
|
||||||
|
|
@ -48,7 +48,7 @@ pub(crate) async fn pip_sync(
|
||||||
require_hashes: bool,
|
require_hashes: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
|
@ -68,7 +68,7 @@ pub(crate) async fn pip_sync(
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.keyring_provider(keyring_provider);
|
.keyring(keyring_provider);
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let RequirementsSpecification {
|
let RequirementsSpecification {
|
||||||
|
|
@ -150,18 +150,13 @@ pub(crate) async fn pip_sync(
|
||||||
let index_locations =
|
let index_locations =
|
||||||
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
index_locations.combine(index_url, extra_index_urls, find_links, no_index);
|
||||||
|
|
||||||
// Add all authenticated sources to the store.
|
|
||||||
for url in index_locations.urls() {
|
|
||||||
GLOBAL_AUTH_STORE.save_from_url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the registry client.
|
// Initialize the registry client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.index_strategy(index_strategy)
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring(keyring_provider)
|
||||||
.markers(venv.interpreter().markers())
|
.markers(venv.interpreter().markers())
|
||||||
.platform(venv.interpreter().platform())
|
.platform(venv.interpreter().platform())
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::{InstalledMetadata, Name};
|
use distribution_types::{InstalledMetadata, Name};
|
||||||
use pep508_rs::{Requirement, RequirementsTxtRequirement, UnnamedRequirement};
|
use pep508_rs::{Requirement, RequirementsTxtRequirement, UnnamedRequirement};
|
||||||
use uv_auth::KeyringProvider;
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
|
||||||
|
|
@ -27,14 +27,14 @@ pub(crate) async fn pip_uninstall(
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
let client_builder = BaseClientBuilder::new()
|
let client_builder = BaseClientBuilder::new()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.keyring_provider(keyring_provider);
|
.keyring(keyring_provider);
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
// Read all requirements from the provided sources.
|
||||||
let spec = RequirementsSpecification::from_simple_sources(sources, &client_builder).await?;
|
let spec = RequirementsSpecification::from_simple_sources(sources, &client_builder).await?;
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ use thiserror::Error;
|
||||||
use distribution_types::{DistributionMetadata, IndexLocations, Name, ResolvedDist};
|
use distribution_types::{DistributionMetadata, IndexLocations, Name, ResolvedDist};
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep508_rs::Requirement;
|
use pep508_rs::Requirement;
|
||||||
use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE};
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_configuration::{ConfigSettings, IndexStrategy, NoBinary, NoBuild, SetupPyStrategy};
|
use uv_configuration::{ConfigSettings, IndexStrategy, NoBinary, NoBuild, SetupPyStrategy};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
|
|
@ -36,7 +36,7 @@ pub(crate) async fn venv(
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -99,7 +99,7 @@ async fn venv_impl(
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
index_strategy: IndexStrategy,
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProviderType,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -147,17 +147,12 @@ async fn venv_impl(
|
||||||
// Extract the interpreter.
|
// Extract the interpreter.
|
||||||
let interpreter = venv.interpreter();
|
let interpreter = venv.interpreter();
|
||||||
|
|
||||||
// Add all authenticated sources to the store.
|
|
||||||
for url in index_locations.urls() {
|
|
||||||
GLOBAL_AUTH_STORE.save_from_url(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instantiate a client.
|
// Instantiate a client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
.index_strategy(index_strategy)
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring(keyring_provider)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.markers(interpreter.markers())
|
.markers(interpreter.markers())
|
||||||
.platform(interpreter.platform())
|
.platform(interpreter.platform())
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ use tracing::instrument;
|
||||||
use distribution_types::IndexLocations;
|
use distribution_types::IndexLocations;
|
||||||
use uv_cache::{Cache, Refresh};
|
use uv_cache::{Cache, Refresh};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::NoBinary;
|
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy, Upgrade};
|
||||||
use uv_configuration::{ConfigSettings, NoBuild, Reinstall, SetupPyStrategy, Upgrade};
|
|
||||||
use uv_requirements::{ExtrasSpecification, RequirementsSource};
|
use uv_requirements::{ExtrasSpecification, RequirementsSource};
|
||||||
use uv_resolver::{DependencyMode, PreReleaseMode};
|
use uv_resolver::{DependencyMode, PreReleaseMode};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue