Provide new unsafe-first-index strategy

This commit is contained in:
John Mumm 2025-05-01 09:36:29 +02:00
parent a261995449
commit c9f2dfbab6
No known key found for this signature in database
GPG key ID: 08EFBF1E7471E2CF
10 changed files with 125 additions and 13 deletions

View file

@ -324,15 +324,21 @@ impl RegistryClient {
let mut results = Vec::new();
match self.index_strategy_for(package_name) {
let index_strategy = self.index_strategy_for(package_name);
match index_strategy {
// If we're searching for the first index that contains the package, fetch serially.
IndexStrategy::FirstIndex => {
IndexStrategy::FirstIndex | IndexStrategy::UnsafeFirstIndex => {
for index in indexes {
let _permit = download_concurrency.acquire().await;
match index.format {
IndexFormat::Simple => {
let status_code_strategy =
self.index_urls.status_code_strategy_for(index.url);
let status_code_strategy = match index_strategy {
IndexStrategy::UnsafeFirstIndex => {
IndexStatusCodeStrategy::ignore_authentication_error_codes()
}
_ => self.index_urls.status_code_strategy_for(index.url),
};
match self
.simple_single_index(
package_name,

View file

@ -323,6 +323,13 @@ pub enum IndexStrategy {
#[default]
#[cfg_attr(feature = "clap", clap(alias = "first-match"))]
FirstIndex,
/// Only use results from the first index that returns a match for a given package name,
/// but ignore any authentication failures.
///
/// This is more secure than pip's behavior but is still vulnerable to dependency confusion
/// if we encounter an authentication failure at an earlier index.
#[cfg_attr(feature = "clap", clap(alias = "unsafe-first-index"))]
UnsafeFirstIndex,
/// Search for every package name across all indexes, exhausting the versions from the first
/// index before moving on to the next.
///

View file

@ -65,17 +65,20 @@ impl IndexStatusCodeStrategy {
index_url: &IndexUrl,
capabilities: &IndexCapabilities,
) -> IndexStatusCodeDecision {
match status_code {
StatusCode::UNAUTHORIZED => {
capabilities.set_unauthorized(index_url.clone());
}
StatusCode::FORBIDDEN => {
capabilities.set_forbidden(index_url.clone());
}
_ => {}
}
match self {
IndexStatusCodeStrategy::Default => match status_code {
StatusCode::NOT_FOUND => IndexStatusCodeDecision::Ignore,
StatusCode::UNAUTHORIZED => {
capabilities.set_unauthorized(index_url.clone());
IndexStatusCodeDecision::Fail(status_code)
}
StatusCode::FORBIDDEN => {
capabilities.set_forbidden(index_url.clone());
IndexStatusCodeDecision::Fail(status_code)
}
StatusCode::UNAUTHORIZED => IndexStatusCodeDecision::Fail(status_code),
StatusCode::FORBIDDEN => IndexStatusCodeDecision::Fail(status_code),
_ => IndexStatusCodeDecision::Fail(status_code),
},
IndexStatusCodeStrategy::IgnoreErrorCodes { status_codes } => {

View file

@ -894,7 +894,10 @@ impl PubGrubReportFormatter<'_> {
// Add hints due to the package being available on an index, but not at the correct version,
// with subsequent indexes that were _not_ queried.
if matches!(selector.index_strategy(), IndexStrategy::FirstIndex) {
if matches!(
selector.index_strategy(),
IndexStrategy::FirstIndex | IndexStrategy::UnsafeFirstIndex
) {
// Do not include the hint if the set is "all versions". This is an unusual but valid
// case in which a package returns a 200 response, but without any versions or
// distributions for the package.

View file

@ -10820,6 +10820,44 @@ fn add_ignore_error_codes() -> Result<()> {
Ok(())
}
/// uv should continue searching the default index if it receives an
/// authentication failure on unsafe-first-index strategy.
#[test]
fn add_unsafe_first_index() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.11, <4"
dependencies = []
[[tool.uv.index]]
name = "my-index"
url = "https://pypi-proxy.fly.dev/basic-auth/simple"
"#
})?;
uv_snapshot!(context.add().arg("anyio").arg("--index-strategy").arg("unsafe-first-index"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"
);
context.assert_command("import anyio").success();
Ok(())
}
/// uv should only fall through on 404s if an empty list is specified
/// in `ignore-error-codes`, even for pytorch.
#[test]
@ -10893,6 +10931,8 @@ fn add_missing_package_on_pytorch() -> Result<()> {
----- stderr -----
× No solution found when resolving dependencies:
Because fakepkg was not found in the package registry and your project depends on fakepkg, we can conclude that your project's requirements are unsatisfiable.
hint: An index URL (https://download.pytorch.org/whl/cpu) could not be queried due to a lack of valid authentication credentials (403 Forbidden).
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
"
);

View file

@ -127,6 +127,9 @@ To opt in to alternate index behaviors, use the`--index-strategy` command-line o
- `first-index` (default): Search for each package across all indexes, limiting the candidate
versions to those present in the first index that contains the package.
- `unsafe-first-index`: Search for each package across all indexes, limiting the candidate versions
to those present in the first index that contains the package. But treat authentication failures
as package not found.
- `unsafe-first-match`: Search for each package across all indexes, but prefer the first index with
a compatible version, even if newer versions are available on other indexes.
- `unsafe-best-match`: Search for each package across all indexes, and select the best version from
@ -232,6 +235,9 @@ ignore-error-codes = [403]
uv will always continue searching across indexes when it encounters a `404 Not Found`. This cannot
be overridden.
It is also possible to use the [unsafe-first-index strategy](#searching-across-multiple-indexes).
But because this approach is less fine-grained, it is more vulnerable to dependency confusion.
### Disabling authentication
To prevent leaking credentials, authentication can be disabled for an index:

View file

@ -111,6 +111,10 @@ supports the following values:
- `first-index` (default): Search for each package across all indexes, limiting the candidate
versions to those present in the first index that contains the package, prioritizing the
`--extra-index-url` indexes over the default index URL.
- `unsafe-first-index`: Search for each package across all indexes, limiting the candidate versions
to those present in the first index that contains the package, prioritizing the
`--extra-index-url` indexes over the default index URL. But treat authentication failures as
package not found.
- `unsafe-first-match`: Search for each package across all indexes, but prefer the first index with
a compatible version, even if newer versions are available on other indexes.
- `unsafe-best-match`: Search for each package across all indexes, and select the best version from

View file

@ -230,6 +230,8 @@ uv run [OPTIONS] [COMMAND]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -905,6 +907,8 @@ uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -1267,6 +1271,8 @@ uv remove [OPTIONS] <PACKAGES>...
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -1639,6 +1645,8 @@ uv sync [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -2017,6 +2025,8 @@ uv lock [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -2359,6 +2369,8 @@ uv export [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -2736,6 +2748,8 @@ uv tree [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -3232,6 +3246,8 @@ uv tool run [OPTIONS] [COMMAND]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -3577,6 +3593,8 @@ uv tool install [OPTIONS] <PACKAGE>
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -3899,6 +3917,8 @@ uv tool upgrade [OPTIONS] <NAME>...
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -5733,6 +5753,8 @@ uv pip compile [OPTIONS] <SRC_FILE|--group <GROUP>>
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -6248,6 +6270,8 @@ uv pip sync [OPTIONS] <SRC_FILE>...
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -6739,6 +6763,8 @@ uv pip install [OPTIONS] <PACKAGE|--requirements <REQUIREMENTS>|--editable <EDIT
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -7529,6 +7555,8 @@ uv pip list [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -7870,6 +7898,8 @@ uv pip tree [OPTIONS]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -8230,6 +8260,8 @@ uv venv [OPTIONS] [PATH]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>
@ -8515,6 +8547,8 @@ uv build [OPTIONS] [SRC]
<ul>
<li><code>first-index</code>: Only use results from the first index that returns a match for a given package name</li>
<li><code>unsafe-first-index</code>: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures</li>
<li><code>unsafe-first-match</code>: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next</li>
<li><code>unsafe-best-match</code>: Search for every package name across all indexes, preferring the &quot;best&quot; version found. If a package version is in multiple indexes, only look at the entry for the first index</li>

View file

@ -1114,6 +1114,7 @@ same name to an alternate index.
**Possible values**:
- `"first-index"`: Only use results from the first index that returns a match for a given package name
- `"unsafe-first-index"`: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures
- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next
- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
@ -2551,6 +2552,7 @@ same name to an alternate index.
**Possible values**:
- `"first-index"`: Only use results from the first index that returns a match for a given package name
- `"unsafe-first-index"`: Only use results from the first index that returns a match for a given package name, but ignore any authentication failures
- `"unsafe-first-match"`: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next
- `"unsafe-best-match"`: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index

7
uv.schema.json generated
View file

@ -948,6 +948,13 @@
"first-index"
]
},
{
"description": "Only use results from the first index that returns a match for a given package name, but ignore any authentication failures.\n\nThis is more secure than pip's behavior but is still vulnerable to dependency confusion if we encounter an authentication failure at an earlier index.",
"type": "string",
"enum": [
"unsafe-first-index"
]
},
{
"description": "Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next.\n\nIn this strategy, we look for every package across all indexes. When resolving, we attempt to use versions from the indexes in order, such that we exhaust all available versions from the first index before moving on to the next. Further, if a version is found to be incompatible in the first index, we do not reconsider that version in subsequent indexes, even if the secondary index might contain compatible versions (e.g., variants of the same versions with different ABI tags or Python version constraints).\n\nSee: <https://peps.python.org/pep-0708/>",
"type": "string",