mirror of https://github.com/astral-sh/uv
Add support for retrieving credentials from `keyring` (#2254)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> Adds basic keyring auth support for `uv` commands. Adds clone of `pip`'s `--keyring-provider subprocess` argument (using CLI `keyring` tool). See issue: https://github.com/astral-sh/uv/issues/1520 ## Test Plan <!-- How was it tested? --> Hard to write full-suite unit tests due to reliance on `process::Command` for `keyring` cli Manually tested end-to-end in a project with GCP artifact registry using keyring password: ```bash ➜ uv pip uninstall watchdog Uninstalled 1 package in 46ms - watchdog==4.0.0 ➜ cargo run -- pip install --index-url https://<redacted>/python/simple/ --extra-index-url https://<redacted>/pypi-mirror/simple/ watchdog Finished dev [unoptimized + debuginfo] target(s) in 0.18s Running `target/debug/uv pip install --index-url 'https://<redacted>/python/simple/' --extra-index-url 'https://<redacted>/pypi-mirror/simple/' watchdog` error: HTTP status client error (401 Unauthorized) for url (https://<redacted>/pypi-mirror/simple/watchdog/) ➜ cargo run -- pip install --keyring-provider subprocess --index-url https://<redacted>/python/simple/ --extra-index-url https://<redacted>/pypi-mirror/simple/ watchdog Finished dev [unoptimized + debuginfo] target(s) in 0.17s Running `target/debug/uv pip install --keyring-provider subprocess --index-url 'https://<redacted>/python/simple/' --extra-index-url 'https://<redacted>/pypi-mirror/simple/' watchdog` Resolved 1 package in 2.34s Installed 1 package in 27ms + watchdog==4.0.0 ``` `requirements.txt` ``` # # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # .bin/generate-requirements # --index-url https://<redacted>/python/simple/ --extra-index-url https://<redacted>/pypi-mirror/simple/ ... ``` ```bash ➜ cargo run -- pip install --keyring-provider subprocess -r requirements.txt Finished dev [unoptimized + debuginfo] target(s) in 0.19s Running `target/debug/uv pip install --keyring-provider subprocess -r requirements.txt` Resolved 205 packages in 23.52s Built <redacted> ... Downloaded 47 packages in 19.32s Installed 195 packages in 276ms + <redacted> ... ``` --------- Co-authored-by: Thomas Gilgenast <thomas@vant.ai> Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
d4d78b0cc3
commit
9159731792
|
|
@ -145,6 +145,16 @@ version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-json-diff"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert_cmd"
|
name = "assert_cmd"
|
||||||
version = "2.0.14"
|
version = "2.0.14"
|
||||||
|
|
@ -857,6 +867,24 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
|
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deadpool"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"deadpool-runtime",
|
||||||
|
"num_cpus",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deadpool-runtime"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derivative"
|
name = "derivative"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -1342,7 +1370,26 @@ dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
|
"indexmap 2.2.5",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.1.0",
|
||||||
"indexmap 2.2.5",
|
"indexmap 2.2.5",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1431,6 +1478,17 @@ dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
@ -1438,7 +1496,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http 1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1470,9 +1551,9 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2 0.3.24",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"http-body",
|
"http-body 0.4.6",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
|
@ -1484,6 +1565,27 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"h2 0.4.2",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"want",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.24.2"
|
version = "0.24.2"
|
||||||
|
|
@ -1491,13 +1593,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"hyper",
|
"hyper 0.14.28",
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
|
"hyper 1.2.0",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.60"
|
version = "0.1.60"
|
||||||
|
|
@ -2787,10 +2905,10 @@ dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2 0.3.24",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"http-body",
|
"http-body 0.4.6",
|
||||||
"hyper",
|
"hyper 0.14.28",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|
@ -2829,7 +2947,7 @@ checksum = "88a3e86aa6053e59030e7ce2d2a3b258dd08fc2d337d52f73f6cb480f5858690"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"task-local-extensions",
|
"task-local-extensions",
|
||||||
|
|
@ -2847,8 +2965,8 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"hyper",
|
"hyper 0.14.28",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
|
|
@ -4134,6 +4252,7 @@ dependencies = [
|
||||||
"tracing-tree",
|
"tracing-tree",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"url",
|
"url",
|
||||||
|
"uv-auth",
|
||||||
"uv-build",
|
"uv-build",
|
||||||
"uv-cache",
|
"uv-cache",
|
||||||
"uv-client",
|
"uv-client",
|
||||||
|
|
@ -4155,8 +4274,20 @@ dependencies = [
|
||||||
name = "uv-auth"
|
name = "uv-auth"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"clap",
|
||||||
|
"lazy_static",
|
||||||
|
"reqwest",
|
||||||
|
"reqwest-middleware",
|
||||||
|
"rust-netrc",
|
||||||
|
"task-local-extensions",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"wiremock",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4217,7 +4348,6 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"async_http_range_reader",
|
"async_http_range_reader",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"base64 0.21.7",
|
|
||||||
"cache-key",
|
"cache-key",
|
||||||
"chrono",
|
"chrono",
|
||||||
"distribution-filename",
|
"distribution-filename",
|
||||||
|
|
@ -4225,8 +4355,8 @@ dependencies = [
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"futures",
|
"futures",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"http",
|
"http 0.2.12",
|
||||||
"hyper",
|
"hyper 0.14.28",
|
||||||
"insta",
|
"insta",
|
||||||
"install-wheel-rs",
|
"install-wheel-rs",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
|
|
@ -4238,7 +4368,6 @@ dependencies = [
|
||||||
"reqwest-retry",
|
"reqwest-retry",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rust-netrc",
|
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
|
|
@ -5060,6 +5189,30 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wiremock"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81"
|
||||||
|
dependencies = [
|
||||||
|
"assert-json-diff",
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"deadpool",
|
||||||
|
"futures",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.2.0",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ indicatif = { version = "0.17.7" }
|
||||||
indoc = { version = "2.0.4" }
|
indoc = { version = "2.0.4" }
|
||||||
itertools = { version = "0.12.1" }
|
itertools = { version = "0.12.1" }
|
||||||
junction = { version = "1.0.0" }
|
junction = { version = "1.0.0" }
|
||||||
|
lazy_static = { version = "1.4.0" }
|
||||||
mailparse = { version = "0.14.0" }
|
mailparse = { version = "0.14.0" }
|
||||||
miette = { version = "6.0.0" }
|
miette = { version = "6.0.0" }
|
||||||
nanoid = { version = "0.4.0" }
|
nanoid = { version = "0.4.0" }
|
||||||
|
|
@ -95,7 +96,7 @@ tempfile = { version = "3.9.0" }
|
||||||
textwrap = { version = "0.16.1" }
|
textwrap = { version = "0.16.1" }
|
||||||
thiserror = { version = "1.0.56" }
|
thiserror = { version = "1.0.56" }
|
||||||
tl = { version = "0.7.7" }
|
tl = { version = "0.7.7" }
|
||||||
tokio = { version = "1.35.1", features = ["rt-multi-thread"] }
|
tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] }
|
||||||
tokio-stream = { version = "0.1.14" }
|
tokio-stream = { version = "0.1.14" }
|
||||||
tokio-tar = { version = "0.3.1" }
|
tokio-tar = { version = "0.3.1" }
|
||||||
tokio-util = { version = "0.7.10", features = ["compat"] }
|
tokio-util = { version = "0.7.10", features = ["compat"] }
|
||||||
|
|
@ -109,6 +110,7 @@ unicode-width = { version = "0.1.11" }
|
||||||
unscanny = { version = "0.1.0" }
|
unscanny = { version = "0.1.0" }
|
||||||
url = { version = "2.5.0" }
|
url = { version = "2.5.0" }
|
||||||
urlencoding = { version = "2.1.3" }
|
urlencoding = { version = "2.1.3" }
|
||||||
|
wiremock = { version = "0.6.0" }
|
||||||
walkdir = { version = "2.5.0" }
|
walkdir = { version = "2.5.0" }
|
||||||
which = { version = "6.0.0" }
|
which = { version = "6.0.0" }
|
||||||
winapi = { version = "0.3.9" }
|
winapi = { version = "0.3.9" }
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use thiserror::Error;
|
||||||
use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError};
|
use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError};
|
||||||
use pypi_types::{DistInfoMetadata, Hashes, Yanked};
|
use pypi_types::{DistInfoMetadata, Hashes, Yanked};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uv_auth::safe_copy_url_auth_to_str;
|
use uv_auth::AuthenticationStore;
|
||||||
|
|
||||||
/// Error converting [`pypi_types::File`] to [`distribution_type::File`].
|
/// Error converting [`pypi_types::File`] to [`distribution_type::File`].
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -53,12 +53,10 @@ impl File {
|
||||||
size: file.size,
|
size: file.size,
|
||||||
upload_time_utc_ms: file.upload_time.map(|dt| dt.timestamp_millis()),
|
upload_time_utc_ms: file.upload_time.map(|dt| dt.timestamp_millis()),
|
||||||
url: if file.url.contains("://") {
|
url: if file.url.contains("://") {
|
||||||
let url = safe_copy_url_auth_to_str(base, &file.url)
|
let url = Url::parse(&file.url)
|
||||||
.map_err(|err| FileConversionError::Url(file.url.clone(), err))?
|
.map_err(|err| FileConversionError::Url(file.url.clone(), err))?;
|
||||||
.map(|url| url.to_string())
|
let url = AuthenticationStore::with_url_encoded_auth(url);
|
||||||
.unwrap_or(file.url);
|
FileLocation::AbsoluteUrl(url.to_string())
|
||||||
|
|
||||||
FileLocation::AbsoluteUrl(url)
|
|
||||||
} else {
|
} else {
|
||||||
FileLocation::RelativeUrl(base.to_string(), file.url)
|
FileLocation::RelativeUrl(base.to_string(), file.url)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,19 @@ version = "0.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
url = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive", "env"], optional = true }
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
reqwest-middleware = { workspace = true }
|
||||||
|
rust-netrc = { workspace = true }
|
||||||
|
task-local-extensions = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
wiremock = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::store::{BasicAuthData, Credential};
|
||||||
|
|
||||||
|
/// Keyring provider to use for authentication
|
||||||
|
///
|
||||||
|
/// See <https://pip.pypa.io/en/stable/topics/authentication/#keyring-support>
|
||||||
|
#[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>
|
||||||
|
pub fn get_keyring_subprocess_auth(url: &Url) -> Result<Option<Credential>, Error> {
|
||||||
|
let host = url.host_str();
|
||||||
|
if host.is_none() {
|
||||||
|
return Err(Error::NotKeyringTarget(
|
||||||
|
"Should only use keyring for urls with host".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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| {
|
||||||
|
password.map(|password| {
|
||||||
|
Credential::Basic(BasicAuthData {
|
||||||
|
username: username.to_string(),
|
||||||
|
password: Some(password),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostless_url_should_err() {
|
||||||
|
let url = Url::parse("file:/etc/bin/").unwrap();
|
||||||
|
let res = get_keyring_subprocess_auth(&url);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert!(matches!(res.unwrap_err(),
|
||||||
|
Error::NotKeyringTarget(s) if s == "Should only use keyring for urls with host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn passworded_url_should_err() {
|
||||||
|
let url = Url::parse("https://u:p@example.com").unwrap();
|
||||||
|
let res = get_keyring_subprocess_auth(&url);
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert!(matches!(res.unwrap_err(),
|
||||||
|
Error::NotKeyringTarget(s) if s == "Url already contains password - keyring not required"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,170 +1,132 @@
|
||||||
/// HTTP authentication utilities.
|
mod keyring;
|
||||||
use tracing::warn;
|
mod middleware;
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
pub use keyring::KeyringProvider;
|
||||||
|
pub use middleware::AuthMiddleware;
|
||||||
|
pub use store::AuthenticationStore;
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Optimized version of [`safe_copy_url_auth`] which avoids parsing a string
|
/// Used to determine if authentication information should be retained on a new URL.
|
||||||
/// into a URL unless the given URL has authentication to copy. Useful for patterns
|
/// Based on the specification defined in RFC 7235 and 7230.
|
||||||
/// where the returned URL would immediately be cast into a string.
|
|
||||||
///
|
|
||||||
/// Returns [`Err`] if there is authentication to copy and `new_url` is not a valid URL.
|
|
||||||
/// Returns [`None`] if there is no authentication to copy.
|
|
||||||
pub fn safe_copy_url_auth_to_str(
|
|
||||||
trusted_url: &Url,
|
|
||||||
new_url: &str,
|
|
||||||
) -> Result<Option<Url>, url::ParseError> {
|
|
||||||
if trusted_url.username().is_empty() && trusted_url.password().is_none() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_url = Url::parse(new_url)?;
|
|
||||||
Ok(Some(safe_copy_url_auth(trusted_url, new_url)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy authentication from one URL to another URL if applicable.
|
|
||||||
///
|
|
||||||
/// See [`should_retain_auth`] for details on when authentication is retained.
|
|
||||||
#[must_use]
|
|
||||||
pub fn safe_copy_url_auth(trusted_url: &Url, mut new_url: Url) -> Url {
|
|
||||||
if should_retain_auth(trusted_url, &new_url) {
|
|
||||||
new_url
|
|
||||||
.set_username(trusted_url.username())
|
|
||||||
.unwrap_or_else(|()| warn!("Failed to transfer username to response URL: {new_url}"));
|
|
||||||
new_url
|
|
||||||
.set_password(trusted_url.password())
|
|
||||||
.unwrap_or_else(|()| warn!("Failed to transfer password to response URL: {new_url}"));
|
|
||||||
}
|
|
||||||
new_url
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine if authentication information should be retained on a new URL.
|
|
||||||
/// Implements the specification defined in RFC 7235 and 7230.
|
|
||||||
///
|
///
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc7235#section-2.2>
|
/// <https://datatracker.ietf.org/doc/html/rfc7235#section-2.2>
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc7230#section-5.5>
|
/// <https://datatracker.ietf.org/doc/html/rfc7230#section-5.5>
|
||||||
fn should_retain_auth(trusted_url: &Url, new_url: &Url) -> bool {
|
//
|
||||||
// The "scheme" and "authority" components must match to retain authentication
|
// The "scheme" and "authority" components must match to retain authentication
|
||||||
// The "authority", is composed of the host and port.
|
// 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 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>,
|
||||||
|
}
|
||||||
|
|
||||||
// Check the scheme.
|
impl From<&Url> for NetLoc {
|
||||||
// The scheme must always be an exact match.
|
fn from(url: &Url) -> Self {
|
||||||
// Note some clients such as Python's `requests` library allow an upgrade
|
Self {
|
||||||
// from `http` to `https` but this is not spec-compliant.
|
scheme: url.scheme().to_string(),
|
||||||
// <https://github.com/pypa/pip/blob/75f54cae9271179b8cc80435f92336c97e349f9d/src/pip/_vendor/requests/sessions.py#L133-L136>
|
host: url.host_str().map(str::to_string),
|
||||||
if trusted_url.scheme() != new_url.scheme() {
|
port: url.port(),
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The host must always be an exact match.
|
|
||||||
if trusted_url.host() != new_url.host() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the port.
|
|
||||||
// The port is only allowed to differ if it it matches the "default port" for the scheme.
|
|
||||||
// However, `reqwest` sets the `port` to `None` if it matches the default port so we do
|
|
||||||
// not need any special handling here.
|
|
||||||
if trusted_url.port() != new_url.port() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
use crate::should_retain_auth;
|
use crate::NetLoc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_retain_auth() -> Result<(), ParseError> {
|
fn test_should_retain_auth() -> Result<(), ParseError> {
|
||||||
// Exact match (https)
|
// Exact match (https)
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com")?),
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Exact match (with port)
|
// Exact match (with port)
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("https://example.com:1234")?,
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
&Url::parse("https://example.com:1234")?,
|
NetLoc::from(&Url::parse("https://example.com:1234")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Exact match (http)
|
// Exact match (http)
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("http://example.com")?,
|
NetLoc::from(&Url::parse("http://example.com")?),
|
||||||
&Url::parse("http://example.com")?,
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Okay, path differs
|
// Okay, path differs
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("http://example.com/foo")?,
|
NetLoc::from(&Url::parse("http://example.com/foo")?),
|
||||||
&Url::parse("http://example.com/bar")?,
|
NetLoc::from(&Url::parse("http://example.com/bar")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Okay, default port differs (https)
|
// Okay, default port differs (https)
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("https://example.com:443")?,
|
NetLoc::from(&Url::parse("https://example.com:443")?),
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
));
|
);
|
||||||
assert!(should_retain_auth(
|
|
||||||
&Url::parse("https://example.com")?,
|
|
||||||
&Url::parse("https://example.com:443")?,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Okay, default port differs (http)
|
// Okay, default port differs (http)
|
||||||
assert!(should_retain_auth(
|
assert_eq!(
|
||||||
&Url::parse("http://example.com:80")?,
|
NetLoc::from(&Url::parse("http://example.com:80")?),
|
||||||
&Url::parse("http://example.com")?,
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
));
|
);
|
||||||
assert!(should_retain_auth(
|
|
||||||
&Url::parse("http://example.com")?,
|
|
||||||
&Url::parse("http://example.com:80")?,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Mismatched scheme
|
// Mismatched scheme
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com")?),
|
||||||
&Url::parse("http://example.com")?,
|
NetLoc::from(&Url::parse("http://example.com")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mismatched scheme, we explicitly do not allow upgrade to https
|
// Mismatched scheme, we explicitly do not allow upgrade to https
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("http://example.com")?,
|
NetLoc::from(&Url::parse("http://example.com")?),
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mismatched host
|
// Mismatched host
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://foo.com")?,
|
NetLoc::from(&Url::parse("https://foo.com")?),
|
||||||
&Url::parse("https://bar.com")?,
|
NetLoc::from(&Url::parse("https://bar.com")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mismatched port
|
// Mismatched port
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://example.com:1234")?,
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
&Url::parse("https://example.com:5678")?,
|
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mismatched port, with one as default for scheme
|
// Mismatched port, with one as default for scheme
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://example.com:443")?,
|
NetLoc::from(&Url::parse("https://example.com:443")?),
|
||||||
&Url::parse("https://example.com:5678")?,
|
NetLoc::from(&Url::parse("https://example.com:5678")?)
|
||||||
));
|
);
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://example.com:1234")?,
|
NetLoc::from(&Url::parse("https://example.com:1234")?),
|
||||||
&Url::parse("https://example.com:443")?,
|
NetLoc::from(&Url::parse("https://example.com:443")?)
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mismatched port, with default for a different scheme
|
// Mismatched port, with default for a different scheme
|
||||||
assert!(!should_retain_auth(
|
assert_ne!(
|
||||||
&Url::parse("https://example.com")?,
|
NetLoc::from(&Url::parse("https://example.com:80")?),
|
||||||
&Url::parse("https://example.com:80")?,
|
NetLoc::from(&Url::parse("https://example.com")?)
|
||||||
));
|
);
|
||||||
assert!(!should_retain_auth(
|
|
||||||
&Url::parse("https://example.com:80")?,
|
|
||||||
&Url::parse("https://example.com")?,
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
use netrc::Netrc;
|
||||||
|
use reqwest::{header::HeaderValue, Request, Response};
|
||||||
|
use reqwest_middleware::{Middleware, Next};
|
||||||
|
use std::path::Path;
|
||||||
|
use task_local_extensions::Extensions;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
keyring::{get_keyring_subprocess_auth, KeyringProvider},
|
||||||
|
store::{AuthenticationStore, Credential},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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>.
|
||||||
|
pub struct AuthMiddleware {
|
||||||
|
nrc: Option<Netrc>,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthMiddleware {
|
||||||
|
pub fn new(keyring_provider: KeyringProvider) -> Self {
|
||||||
|
Self {
|
||||||
|
nrc: Netrc::new().ok(),
|
||||||
|
keyring_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_netrc_file(file: &Path, keyring_provider: KeyringProvider) -> Self {
|
||||||
|
Self {
|
||||||
|
nrc: Netrc::from_file(file).ok(),
|
||||||
|
keyring_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Middleware for AuthMiddleware {
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
mut req: Request,
|
||||||
|
_extensions: &mut Extensions,
|
||||||
|
next: Next<'_>,
|
||||||
|
) -> reqwest_middleware::Result<Response> {
|
||||||
|
let url = req.url().clone();
|
||||||
|
// If the request already has an authorization header, we don't need to do anything.
|
||||||
|
// This gives in-URL credentials precedence over the netrc file.
|
||||||
|
if req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
||||||
|
if !url.username().is_empty() {
|
||||||
|
AuthenticationStore::save_from_url(&url);
|
||||||
|
}
|
||||||
|
return next.run(req, _extensions).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try auth strategies in order of precedence:
|
||||||
|
if let Some(stored_auth) = AuthenticationStore::get(&url) {
|
||||||
|
// If we've already seen this URL, we can use the stored credentials
|
||||||
|
if let Some(auth) = stored_auth {
|
||||||
|
match auth {
|
||||||
|
Credential::Basic(_) => {
|
||||||
|
req.headers_mut().insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
basic_auth(auth.username(), auth.password()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Url must already have auth if before middleware runs - see `AuthenticationStore::with_url_encoded_auth`
|
||||||
|
Credential::UrlEncoded(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(auth) = self.nrc.as_ref().and_then(|nrc| {
|
||||||
|
// If we find a matching entry in the netrc file, we can use it
|
||||||
|
url.host_str()
|
||||||
|
.and_then(|host| nrc.hosts.get(host).or_else(|| nrc.hosts.get("default")))
|
||||||
|
}) {
|
||||||
|
let auth = Credential::from(auth.to_owned());
|
||||||
|
req.headers_mut().insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
basic_auth(auth.username(), auth.password()),
|
||||||
|
);
|
||||||
|
AuthenticationStore::set(&url, Some(auth));
|
||||||
|
} else if matches!(self.keyring_provider, KeyringProvider::Subprocess) {
|
||||||
|
// If we have keyring support enabled, we check there as well
|
||||||
|
match get_keyring_subprocess_auth(&url) {
|
||||||
|
Ok(Some(auth)) => {
|
||||||
|
req.headers_mut().insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
basic_auth(auth.username(), auth.password()),
|
||||||
|
);
|
||||||
|
AuthenticationStore::set(&url, Some(auth));
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
debug!("No keyring credentials found for {url}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to get keyring credentials for {url}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still don't have any credentials, we save the URL so we don't have to check netrc or keyring again
|
||||||
|
if !req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
||||||
|
AuthenticationStore::set(&url, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
next.run(req, _extensions).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `HeaderValue` for basic authentication.
|
||||||
|
///
|
||||||
|
/// 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();
|
||||||
|
{
|
||||||
|
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
|
||||||
|
let _ = write!(encoder, "{}:", username);
|
||||||
|
if let Some(password) = password {
|
||||||
|
let _ = write!(encoder, "{}", password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
|
||||||
|
header.set_sensitive(true);
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use reqwest::Client;
|
||||||
|
use reqwest_middleware::ClientBuilder;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use wiremock::matchers::{basic_auth, method, path};
|
||||||
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
|
const NETRC: &str = r#"default login myuser password mypassword"#;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_init() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/hello"))
|
||||||
|
.and(basic_auth("myuser", "mypassword"))
|
||||||
|
.respond_with(ResponseTemplate::new(200))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let status = ClientBuilder::new(Client::builder().build()?)
|
||||||
|
.build()
|
||||||
|
.get(format!("{}/hello", &server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status();
|
||||||
|
|
||||||
|
assert_eq!(status, 404);
|
||||||
|
|
||||||
|
let mut netrc_file = NamedTempFile::new()?;
|
||||||
|
writeln!(netrc_file, "{}", NETRC)?;
|
||||||
|
|
||||||
|
let status = ClientBuilder::new(Client::builder().build()?)
|
||||||
|
.with(AuthMiddleware::from_netrc_file(
|
||||||
|
netrc_file.path(),
|
||||||
|
KeyringProvider::Disabled,
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
.get(format!("{}/hello", &server.uri()))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status();
|
||||||
|
|
||||||
|
assert_eq!(status, 200);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
|
use netrc::Authenticator;
|
||||||
|
use tracing::warn;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::NetLoc;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
// Store credentials for NetLoc
|
||||||
|
static ref PASSWORDS: Mutex<HashMap<NetLoc, Option<Credential>>> = Mutex::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Credential {
|
||||||
|
Basic(BasicAuthData),
|
||||||
|
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;
|
||||||
|
|
||||||
|
impl AuthenticationStore {
|
||||||
|
pub fn get(url: &Url) -> Option<Option<Credential>> {
|
||||||
|
let netloc = NetLoc::from(url);
|
||||||
|
let passwords = PASSWORDS.lock().unwrap();
|
||||||
|
passwords.get(&netloc).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(url: &Url, auth: Option<Credential>) {
|
||||||
|
let netloc = NetLoc::from(url);
|
||||||
|
let mut passwords = PASSWORDS.lock().unwrap();
|
||||||
|
passwords.insert(netloc, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy authentication from one URL to another URL if applicable.
|
||||||
|
pub fn with_url_encoded_auth(url: Url) -> Url {
|
||||||
|
let netloc = NetLoc::from(&url);
|
||||||
|
let passwords = PASSWORDS.lock().unwrap();
|
||||||
|
if let Some(Some(Credential::UrlEncoded(url_auth))) = passwords.get(&netloc) {
|
||||||
|
url_auth.apply_to_url(url)
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_from_url(url: &Url) {
|
||||||
|
let netloc = NetLoc::from(url);
|
||||||
|
let mut passwords = PASSWORDS.lock().unwrap();
|
||||||
|
if url.username().is_empty() {
|
||||||
|
// No credentials to save
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let auth = UrlAuthData {
|
||||||
|
username: url.username().to_string(),
|
||||||
|
password: url.password().map(str::to_string),
|
||||||
|
};
|
||||||
|
passwords.insert(netloc, Some(Credential::UrlEncoded(auth)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// NOTE: Because tests run in parallel, it is imperative to use different URLs for each
|
||||||
|
#[test]
|
||||||
|
fn set_get_work() {
|
||||||
|
let url = Url::parse("https://test1example1.com/simple/").unwrap();
|
||||||
|
let not_set_res = AuthenticationStore::get(&url);
|
||||||
|
assert!(not_set_res.is_none());
|
||||||
|
|
||||||
|
let found_first_url = Url::parse("https://test1example2.com/simple/first/").unwrap();
|
||||||
|
let not_found_first_url = Url::parse("https://test1example3.com/simple/first/").unwrap();
|
||||||
|
|
||||||
|
AuthenticationStore::set(
|
||||||
|
&found_first_url,
|
||||||
|
Some(Credential::Basic(BasicAuthData {
|
||||||
|
username: "u".to_string(),
|
||||||
|
password: Some("p".to_string()),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
AuthenticationStore::set(¬_found_first_url, None);
|
||||||
|
|
||||||
|
let found_second_url = Url::parse("https://test1example2.com/simple/second/").unwrap();
|
||||||
|
let not_found_second_url = Url::parse("https://test1example3.com/simple/second/").unwrap();
|
||||||
|
|
||||||
|
let found_res = AuthenticationStore::get(&found_second_url);
|
||||||
|
assert!(found_res.is_some());
|
||||||
|
let found_res = found_res.unwrap();
|
||||||
|
assert!(matches!(found_res, Some(Credential::Basic(_))));
|
||||||
|
|
||||||
|
let not_found_res = AuthenticationStore::get(¬_found_second_url);
|
||||||
|
assert!(not_found_res.is_some());
|
||||||
|
let not_found_res = not_found_res.unwrap();
|
||||||
|
assert!(not_found_res.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_url_encoded_auth_works() {
|
||||||
|
let url = Url::parse("https://test2example.com/simple/").unwrap();
|
||||||
|
let auth = Credential::UrlEncoded(UrlAuthData {
|
||||||
|
username: "u".to_string(),
|
||||||
|
password: Some("p".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
AuthenticationStore::set(&url, Some(auth.clone()));
|
||||||
|
|
||||||
|
let url = AuthenticationStore::with_url_encoded_auth(url);
|
||||||
|
assert_eq!(url.username(), "u");
|
||||||
|
assert_eq!(url.password(), Some("p"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_from_url_works() {
|
||||||
|
let url = Url::parse("https://u:p@test3example.com/simple/").unwrap();
|
||||||
|
|
||||||
|
AuthenticationStore::save_from_url(&url);
|
||||||
|
|
||||||
|
let found_res = AuthenticationStore::get(&url);
|
||||||
|
assert!(found_res.is_some());
|
||||||
|
let found_res = found_res.unwrap();
|
||||||
|
assert!(matches!(found_res, Some(Credential::UrlEncoded(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,6 @@ anyhow = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
async_http_range_reader = { workspace = true }
|
async_http_range_reader = { workspace = true }
|
||||||
async_zip = { workspace = true, features = ["tokio"] }
|
async_zip = { workspace = true, features = ["tokio"] }
|
||||||
base64 = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|
@ -34,7 +33,6 @@ reqwest-middleware = { workspace = true }
|
||||||
reqwest-retry = { workspace = true }
|
reqwest-retry = { workspace = true }
|
||||||
rkyv = { workspace = true, features = ["strict", "validation"] }
|
rkyv = { workspace = true, features = ["strict", "validation"] }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
rust-netrc = { workspace = true }
|
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use pep440_rs::Version;
|
||||||
use pep508_rs::VerbatimUrl;
|
use pep508_rs::VerbatimUrl;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Hashes;
|
use pypi_types::Hashes;
|
||||||
use uv_auth::safe_copy_url_auth;
|
use uv_auth::AuthenticationStore;
|
||||||
use uv_cache::{Cache, CacheBucket};
|
use uv_cache::{Cache, CacheBucket};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
|
|
@ -157,13 +157,13 @@ impl<'a> FlatIndexClient<'a> {
|
||||||
async {
|
async {
|
||||||
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
||||||
// This ensures that we handle redirects and other URL transformations correctly.
|
// This ensures that we handle redirects and other URL transformations correctly.
|
||||||
let url = safe_copy_url_auth(url, response.url().clone());
|
let url = AuthenticationStore::with_url_encoded_auth(response.url().clone());
|
||||||
|
|
||||||
let text = response.text().await.map_err(ErrorKind::from)?;
|
let text = response.text().await.map_err(ErrorKind::from)?;
|
||||||
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
||||||
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
||||||
|
|
||||||
let base = safe_copy_url_auth(&url, base.into_url());
|
let base = AuthenticationStore::with_url_encoded_auth(base.into_url());
|
||||||
let files: Vec<File> = files
|
let files: Vec<File> = files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|file| {
|
.filter_map(|file| {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use http::HeaderValue;
|
|
||||||
use netrc::{Netrc, Result};
|
|
||||||
use reqwest::{Request, Response};
|
use reqwest::{Request, Response};
|
||||||
use reqwest_middleware::{Middleware, Next};
|
use reqwest_middleware::{Middleware, Next};
|
||||||
use task_local_extensions::Extensions;
|
use task_local_extensions::Extensions;
|
||||||
|
|
@ -47,77 +45,3 @@ impl Middleware for OfflineMiddleware {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A middleware with support for netrc files.
|
|
||||||
///
|
|
||||||
/// Based on: <https://github.com/gribouille/netrc>.
|
|
||||||
pub(crate) struct NetrcMiddleware {
|
|
||||||
nrc: Netrc,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetrcMiddleware {
|
|
||||||
pub(crate) fn new() -> Result<Self> {
|
|
||||||
Netrc::new().map(|nrc| NetrcMiddleware { nrc })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl Middleware for NetrcMiddleware {
|
|
||||||
async fn handle(
|
|
||||||
&self,
|
|
||||||
mut req: Request,
|
|
||||||
_extensions: &mut Extensions,
|
|
||||||
next: Next<'_>,
|
|
||||||
) -> reqwest_middleware::Result<Response> {
|
|
||||||
// If the request already has an authorization header, we don't need to do anything.
|
|
||||||
// This gives in-URL credentials precedence over the netrc file.
|
|
||||||
if req.headers().contains_key(reqwest::header::AUTHORIZATION) {
|
|
||||||
return next.run(req, _extensions).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(auth) = req.url().host_str().and_then(|host| {
|
|
||||||
self.nrc
|
|
||||||
.hosts
|
|
||||||
.get(host)
|
|
||||||
.or_else(|| self.nrc.hosts.get("default"))
|
|
||||||
}) {
|
|
||||||
req.headers_mut().insert(
|
|
||||||
reqwest::header::AUTHORIZATION,
|
|
||||||
basic_auth(
|
|
||||||
&auth.login,
|
|
||||||
if auth.password.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&auth.password)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
next.run(req, _extensions).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a `HeaderValue` for basic authentication.
|
|
||||||
///
|
|
||||||
/// 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();
|
|
||||||
{
|
|
||||||
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
|
|
||||||
let _ = write!(encoder, "{}:", username);
|
|
||||||
if let Some(password) = password {
|
|
||||||
let _ = write!(encoder, "{}", password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
|
|
||||||
header.set_sensitive(true);
|
|
||||||
header
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use distribution_types::{BuiltDist, File, FileLocation, IndexUrl, IndexUrls, Nam
|
||||||
use install_wheel_rs::metadata::{find_archive_dist_info, is_metadata_entry};
|
use install_wheel_rs::metadata::{find_archive_dist_info, is_metadata_entry};
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pypi_types::{Metadata23, SimpleJson};
|
use pypi_types::{Metadata23, SimpleJson};
|
||||||
use uv_auth::safe_copy_url_auth;
|
use uv_auth::{AuthMiddleware, AuthenticationStore, KeyringProvider};
|
||||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
@ -30,7 +30,7 @@ use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
use crate::cached_client::CacheControl;
|
use crate::cached_client::CacheControl;
|
||||||
use crate::html::SimpleHtml;
|
use crate::html::SimpleHtml;
|
||||||
use crate::middleware::{NetrcMiddleware, OfflineMiddleware};
|
use crate::middleware::OfflineMiddleware;
|
||||||
use crate::remote_metadata::wheel_metadata_from_remote_zip;
|
use crate::remote_metadata::wheel_metadata_from_remote_zip;
|
||||||
use crate::rkyvutil::OwnedArchive;
|
use crate::rkyvutil::OwnedArchive;
|
||||||
use crate::tls::Roots;
|
use crate::tls::Roots;
|
||||||
|
|
@ -40,6 +40,7 @@ use crate::{tls, CachedClient, CachedClientError, Error, ErrorKind};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RegistryClientBuilder {
|
pub struct RegistryClientBuilder {
|
||||||
index_urls: IndexUrls,
|
index_urls: IndexUrls,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
retries: u32,
|
retries: u32,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -51,6 +52,7 @@ impl RegistryClientBuilder {
|
||||||
pub fn new(cache: Cache) -> Self {
|
pub fn new(cache: Cache) -> Self {
|
||||||
Self {
|
Self {
|
||||||
index_urls: IndexUrls::default(),
|
index_urls: IndexUrls::default(),
|
||||||
|
keyring_provider: KeyringProvider::default(),
|
||||||
native_tls: false,
|
native_tls: false,
|
||||||
cache,
|
cache,
|
||||||
connectivity: Connectivity::Online,
|
connectivity: Connectivity::Online,
|
||||||
|
|
@ -67,6 +69,12 @@ impl RegistryClientBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
||||||
|
self.keyring_provider = keyring_provider;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn connectivity(mut self, connectivity: Connectivity) -> Self {
|
pub fn connectivity(mut self, connectivity: Connectivity) -> Self {
|
||||||
self.connectivity = connectivity;
|
self.connectivity = connectivity;
|
||||||
|
|
@ -159,12 +167,8 @@ impl RegistryClientBuilder {
|
||||||
let retry_strategy = RetryTransientMiddleware::new_with_policy(retry_policy);
|
let retry_strategy = RetryTransientMiddleware::new_with_policy(retry_policy);
|
||||||
let client = client.with(retry_strategy);
|
let client = client.with(retry_strategy);
|
||||||
|
|
||||||
// Initialize the netrc middleware.
|
// Initialize the authentication middleware to set headers.
|
||||||
let client = if let Ok(netrc) = NetrcMiddleware::new() {
|
let client = client.with(AuthMiddleware::new(self.keyring_provider));
|
||||||
client.with(netrc)
|
|
||||||
} else {
|
|
||||||
client
|
|
||||||
};
|
|
||||||
|
|
||||||
client.build()
|
client.build()
|
||||||
}
|
}
|
||||||
|
|
@ -313,7 +317,7 @@ impl RegistryClient {
|
||||||
async {
|
async {
|
||||||
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
// Use the response URL, rather than the request URL, as the base for relative URLs.
|
||||||
// This ensures that we handle redirects and other URL transformations correctly.
|
// This ensures that we handle redirects and other URL transformations correctly.
|
||||||
let url = safe_copy_url_auth(&url, response.url().clone());
|
let url = AuthenticationStore::with_url_encoded_auth(response.url().clone());
|
||||||
|
|
||||||
let content_type = response
|
let content_type = response
|
||||||
.headers()
|
.headers()
|
||||||
|
|
@ -342,7 +346,7 @@ impl RegistryClient {
|
||||||
let text = response.text().await.map_err(ErrorKind::from)?;
|
let text = response.text().await.map_err(ErrorKind::from)?;
|
||||||
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
let SimpleHtml { base, files } = SimpleHtml::parse(&text, &url)
|
||||||
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
.map_err(|err| Error::from_html_err(err, url.clone()))?;
|
||||||
let base = safe_copy_url_auth(&url, base.into_url());
|
let base = AuthenticationStore::with_url_encoded_auth(base.into_url());
|
||||||
|
|
||||||
SimpleMetadata::from_files(files, package_name, &base)
|
SimpleMetadata::from_files(files, package_name, &base)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pep508_rs = { path = "../pep508-rs" }
|
||||||
platform-tags = { path = "../platform-tags" }
|
platform-tags = { path = "../platform-tags" }
|
||||||
pypi-types = { path = "../pypi-types" }
|
pypi-types = { path = "../pypi-types" }
|
||||||
requirements-txt = { path = "../requirements-txt", features = ["reqwest"] }
|
requirements-txt = { path = "../requirements-txt", features = ["reqwest"] }
|
||||||
|
uv-auth = { path = "../uv-auth", features = ["clap"] }
|
||||||
uv-build = { path = "../uv-build" }
|
uv-build = { path = "../uv-build" }
|
||||||
uv-cache = { path = "../uv-cache", features = ["clap"] }
|
uv-cache = { path = "../uv-cache", features = ["clap"] }
|
||||||
uv-client = { path = "../uv-client" }
|
uv-client = { path = "../uv-client" }
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use tracing::debug;
|
||||||
use distribution_types::{IndexLocations, LocalEditable, Verbatim};
|
use distribution_types::{IndexLocations, LocalEditable, Verbatim};
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
|
use uv_auth::KeyringProvider;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
|
@ -58,6 +59,7 @@ pub(crate) async fn pip_compile(
|
||||||
include_index_url: bool,
|
include_index_url: bool,
|
||||||
include_find_links: bool,
|
include_find_links: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: ConfigSettings,
|
config_settings: ConfigSettings,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -190,6 +192,7 @@ pub(crate) async fn pip_compile(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.keyring_provider(keyring_provider)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Resolve the flat indexes from `--find-links`.
|
// Resolve the flat indexes from `--find-links`.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use pep508_rs::{MarkerEnvironment, Requirement};
|
||||||
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;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
|
@ -54,6 +55,7 @@ pub(crate) async fn pip_install(
|
||||||
dependency_mode: DependencyMode,
|
dependency_mode: DependencyMode,
|
||||||
upgrade: Upgrade,
|
upgrade: Upgrade,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
reinstall: &Reinstall,
|
reinstall: &Reinstall,
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
compile: bool,
|
compile: bool,
|
||||||
|
|
@ -184,6 +186,7 @@ pub(crate) async fn pip_install(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.keyring_provider(keyring_provider)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Resolve the flat indexes from `--find-links`.
|
// Resolve the flat indexes from `--find-links`.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ 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;
|
||||||
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache};
|
use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache};
|
||||||
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
|
@ -34,6 +35,7 @@ pub(crate) async fn pip_sync(
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
compile: bool,
|
compile: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
config_settings: &ConfigSettings,
|
config_settings: &ConfigSettings,
|
||||||
|
|
@ -118,6 +120,7 @@ pub(crate) async fn pip_sync(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.keyring_provider(keyring_provider)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Resolve the flat indexes from `--find-links`.
|
// Resolve the flat indexes from `--find-links`.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use distribution_types::{DistributionMetadata, IndexLocations, Name};
|
use distribution_types::{DistributionMetadata, IndexLocations, Name};
|
||||||
use pep508_rs::Requirement;
|
use pep508_rs::Requirement;
|
||||||
|
use uv_auth::KeyringProvider;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
|
|
@ -32,6 +33,7 @@ pub(crate) async fn venv(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -44,6 +46,7 @@ pub(crate) async fn venv(
|
||||||
path,
|
path,
|
||||||
python_request,
|
python_request,
|
||||||
index_locations,
|
index_locations,
|
||||||
|
keyring_provider,
|
||||||
prompt,
|
prompt,
|
||||||
system_site_packages,
|
system_site_packages,
|
||||||
connectivity,
|
connectivity,
|
||||||
|
|
@ -87,6 +90,7 @@ async fn venv_impl(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
|
@ -136,6 +140,7 @@ async fn venv_impl(
|
||||||
// Instantiate a client.
|
// Instantiate a client.
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.keyring_provider(keyring_provider)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use tracing::instrument;
|
||||||
|
|
||||||
use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl};
|
use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl};
|
||||||
use requirements::ExtrasSpecification;
|
use requirements::ExtrasSpecification;
|
||||||
|
use uv_auth::KeyringProvider;
|
||||||
use uv_cache::{Cache, CacheArgs, Refresh};
|
use uv_cache::{Cache, CacheArgs, Refresh};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_installer::{NoBinary, Reinstall};
|
use uv_installer::{NoBinary, Reinstall};
|
||||||
|
|
@ -358,6 +359,13 @@ struct PipCompileArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
|
|
||||||
/// Locations to search for candidate distributions, beyond those found in the indexes.
|
/// Locations to search for candidate distributions, beyond those found in the indexes.
|
||||||
///
|
///
|
||||||
/// If a path, the target must be a directory that contains package as wheel files (`.whl`) or
|
/// If a path, the target must be a directory that contains package as wheel files (`.whl`) or
|
||||||
|
|
@ -525,6 +533,13 @@ struct PipSyncArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
|
///
|
||||||
|
/// Function's similar to `pip`'s `--keyring-provider subprocess` argument,
|
||||||
|
/// `uv` will try to use `keyring` via CLI when this flag is used.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
|
|
||||||
/// The Python interpreter into which packages should be installed.
|
/// The Python interpreter into which packages should be installed.
|
||||||
///
|
///
|
||||||
/// By default, `uv` installs into the virtual environment in the current working directory or
|
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||||
|
|
@ -776,6 +791,13 @@ struct PipInstallArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
|
keyring_provider: KeyringProvider,
|
||||||
|
|
||||||
/// The Python interpreter into which packages should be installed.
|
/// The Python interpreter into which packages should be installed.
|
||||||
///
|
///
|
||||||
/// By default, `uv` installs into the virtual environment in the current working directory or
|
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||||
|
|
@ -1218,6 +1240,13 @@ struct VenvArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")]
|
||||||
|
keyring_provider: uv_auth::KeyringProvider,
|
||||||
|
|
||||||
/// Run offline, i.e., without accessing the network.
|
/// Run offline, i.e., without accessing the network.
|
||||||
#[arg(global = true, long)]
|
#[arg(global = true, long)]
|
||||||
offline: bool,
|
offline: bool,
|
||||||
|
|
@ -1424,6 +1453,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.emit_index_url,
|
args.emit_index_url,
|
||||||
args.emit_find_links,
|
args.emit_find_links,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.keyring_provider,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
if args.offline {
|
if args.offline {
|
||||||
|
|
@ -1479,6 +1509,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.link_mode,
|
args.link_mode,
|
||||||
args.compile,
|
args.compile,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.keyring_provider,
|
||||||
setup_py,
|
setup_py,
|
||||||
if args.offline {
|
if args.offline {
|
||||||
Connectivity::Offline
|
Connectivity::Offline
|
||||||
|
|
@ -1571,6 +1602,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
dependency_mode,
|
dependency_mode,
|
||||||
upgrade,
|
upgrade,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.keyring_provider,
|
||||||
&reinstall,
|
&reinstall,
|
||||||
args.link_mode,
|
args.link_mode,
|
||||||
args.compile,
|
args.compile,
|
||||||
|
|
@ -1694,6 +1726,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
&args.name,
|
&args.name,
|
||||||
args.python.as_deref(),
|
args.python.as_deref(),
|
||||||
&index_locations,
|
&index_locations,
|
||||||
|
args.keyring_provider,
|
||||||
uv_virtualenv::Prompt::from_args(prompt),
|
uv_virtualenv::Prompt::from_args(prompt),
|
||||||
args.system_site_packages,
|
args.system_site_packages,
|
||||||
if args.offline {
|
if args.offline {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue