diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 38c268f294de3..82fb55b2dec33 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -250,11 +250,15 @@ impl Middleware for AuthMiddleware { // If we don't fail with authorization related codes or // authentication policy is Never, return the response. - if !matches!( - response.status(), - StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED - ) || matches!(auth_policy, AuthPolicy::Never) + if matches!(auth_policy, AuthPolicy::Never) + || !matches!( + response.status(), + StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED + ) { + if credentials.is_none() && response.status() == StatusCode::UNAUTHORIZED { + return Err(missing_credentials_error(&url)); + } return Ok(response); } @@ -323,12 +327,11 @@ impl Middleware for AuthMiddleware { } if let Some(response) = response { - Ok(response) - } else { - Err(Error::Middleware(format_err!( - "Missing credentials for {url}" - ))) + if !(response.status() == StatusCode::UNAUTHORIZED && credentials.is_none()) { + return Ok(response); + } } + Err(missing_credentials_error(&url)) } } @@ -527,6 +530,10 @@ fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> String { } } +fn missing_credentials_error(url: &str) -> reqwest_middleware::Error { + Error::Middleware(format_err!("Missing credentials for {url}")) +} + #[cfg(test)] mod tests { use std::io::Write; @@ -575,22 +582,22 @@ mod tests { .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) .build(); - assert_eq!( + assert!( client .get(format!("{}/foo", server.uri())) .send() - .await? - .status(), - 401 + .await + .is_err(), + "Requests should require credentials" ); - assert_eq!( + assert!( client .get(format!("{}/bar", server.uri())) .send() - .await? - .status(), - 401 + .await + .is_err(), + "Requests should require credentials" ); Ok(()) @@ -865,9 +872,8 @@ mod tests { ) .build(); - assert_eq!( - client.get(server.uri()).send().await?.status(), - 401, + assert!( + client.get(server.uri()).send().await.is_err(), "Credentials should not be pulled from the netrc file due to host mismatch" ); @@ -949,9 +955,8 @@ mod tests { ) .build(); - assert_eq!( - client.get(server.uri()).send().await?.status(), - 401, + assert!( + client.get(server.uri()).send().await.is_err(), "Credentials are not pulled from the keyring without a username" ); @@ -1237,14 +1242,12 @@ mod tests { .build(); // Both servers do not work without a username - assert_eq!( - client.get(server_1.uri()).send().await?.status(), - 401, + assert!( + client.get(server_1.uri()).send().await.is_err(), "Requests should require a username" ); - assert_eq!( - client.get(server_2.uri()).send().await?.status(), - 401, + assert!( + client.get(server_2.uri()).send().await.is_err(), "Requests should require a username" ); @@ -1255,9 +1258,8 @@ mod tests { 200, "Requests with a username should succeed" ); - assert_eq!( - client.get(server_2.uri()).send().await?.status(), - 401, + assert!( + client.get(server_2.uri()).send().await.is_err(), "Credentials should not be re-used for the second server" ); @@ -1487,14 +1489,12 @@ mod tests { .build(); // Both servers do not work without a username - assert_eq!( - client.get(base_url_1.clone()).send().await?.status(), - 401, + assert!( + client.get(base_url_1.clone()).send().await.is_err(), "Requests should require a username" ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, + assert!( + client.get(base_url_2.clone()).send().await.is_err(), "Requests should require a username" ); @@ -1602,14 +1602,12 @@ mod tests { .build(); // Both servers do not work without a username - assert_eq!( - client.get(base_url_1.clone()).send().await?.status(), - 401, + assert!( + client.get(base_url_1.clone()).send().await.is_err(), "Requests should require a username" ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, + assert!( + client.get(base_url_2.clone()).send().await.is_err(), "Requests should require a username" ); @@ -1795,13 +1793,12 @@ mod tests { url.set_username(username).unwrap(); url.set_password(Some(password)).unwrap(); - assert_eq!( + assert!( client .get(format!("{}/foo", server.uri())) .send() - .await? - .status(), - 401, + .await + .is_err(), "Requests should not be completed if credentials are required" ); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 4f8b6cccd89c0..5d4d211ba999e 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -10358,7 +10358,7 @@ fn add_auth_policy_never_with_url_credentials() -> Result<()> { ----- stderr ----- error: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl.metadata` - Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl.metadata) + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/files/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl.metadata " ); @@ -10391,15 +10391,12 @@ fn add_auth_policy_never_with_env_var_credentials() -> Result<()> { .env("UV_INDEX_MY_INDEX_USERNAME", "public") .env("UV_INDEX_MY_INDEX_PASSWORD", "heron"), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable. - - hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). - help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing. + error: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/simple/anyio/` + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/simple/anyio/ " ); diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index d6fcb69d6ce6e..db3f3c8922fd2 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7517,16 +7517,15 @@ fn lock_index_workspace_member() -> Result<()> { )?; // Locking without the necessary credentials should fail. - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because iniconfig was not found in the package registry and child depends on iniconfig>=2, we can conclude that child's requirements are unsatisfiable. - And because your workspace requires child, we can conclude that your workspace's requirements are unsatisfiable. - "###); + error: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/` + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ + "); uv_snapshot!(context.filters(), context.lock() .env("UV_INDEX_MY_INDEX_USERNAME", "public") @@ -7870,7 +7869,7 @@ fn lock_redact_https() -> Result<()> { // Installing from the lockfile should fail without credentials. Omit the root, so that we fail // when installing `iniconfig`, rather than when building `foo`. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--index-url").arg("https://pypi-proxy.fly.dev/basic-auth/simple").arg("--no-install-project"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--index-url").arg("https://pypi-proxy.fly.dev/basic-auth/simple").arg("--no-install-project"), @r" success: false exit_code: 1 ----- stdout ----- @@ -7878,12 +7877,12 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- × Failed to download `iniconfig==2.0.0` ├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` - ╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + ╰─▶ Missing credentials for https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig` - "###); + "); // Installing from the lockfile should fail without an index. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-install-project"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-install-project"), @r" success: false exit_code: 1 ----- stdout ----- @@ -7891,9 +7890,9 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- × Failed to download `iniconfig==2.0.0` ├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` - ╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + ╰─▶ Missing credentials for https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig` - "###); + "); // Installing from the lockfile should succeed when credentials are included on the command-line. uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--index-url").arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple"), @r###" @@ -7921,7 +7920,7 @@ fn lock_redact_https() -> Result<()> { "###); // Installing without credentials will fail without a cache. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache").arg("--no-install-project"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache").arg("--no-install-project"), @r" success: false exit_code: 1 ----- stdout ----- @@ -7929,9 +7928,9 @@ fn lock_redact_https() -> Result<()> { ----- stderr ----- × Failed to download `iniconfig==2.0.0` ├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` - ╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + ╰─▶ Missing credentials for https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig` - "###); + "); // Installing with credentials from with `UV_INDEX_URL` should succeed. uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache").env(EnvVars::UV_INDEX_URL, "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple"), @r###" @@ -8459,17 +8458,15 @@ fn lock_env_credentials() -> Result<()> { )?; // Without credentials, the resolution should fail. - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. - - hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized). - "###); + error: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/` + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/simple/iniconfig/ + "); // Provide credentials via environment variables. uv_snapshot!(context.filters(), context.lock()