diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 042adad95..ec8106135 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -196,6 +196,7 @@ impl Middleware for AuthMiddleware { extensions, next, &url, + auth_policy, ) .await; } @@ -212,7 +213,9 @@ impl Middleware for AuthMiddleware { // If it's fully authenticated, finish the request if credentials.password().is_some() { trace!("Request for {url} is fully authenticated"); - return self.complete_request(None, request, extensions, next).await; + return self + .complete_request(None, request, extensions, next, auth_policy) + .await; } // If we just found a username, we'll make the request then look for password elsewhere @@ -286,7 +289,7 @@ impl Middleware for AuthMiddleware { trace!("Retrying request for {url} with credentials from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request); return self - .complete_request(None, retry_request, extensions, next) + .complete_request(None, retry_request, extensions, next, auth_policy) .await; } } @@ -300,7 +303,13 @@ impl Middleware for AuthMiddleware { retry_request = credentials.authenticate(retry_request); trace!("Retrying request for {url} with {credentials:?}"); return self - .complete_request(Some(credentials), retry_request, extensions, next) + .complete_request( + Some(credentials), + retry_request, + extensions, + next, + auth_policy, + ) .await; } @@ -309,7 +318,7 @@ impl Middleware for AuthMiddleware { trace!("Retrying request for {url} with username from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request); return self - .complete_request(None, retry_request, extensions, next) + .complete_request(None, retry_request, extensions, next, auth_policy) .await; } } @@ -334,13 +343,16 @@ impl AuthMiddleware { request: Request, extensions: &mut Extensions, next: Next<'_>, + auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let Some(credentials) = credentials else { // Nothing to insert into the cache if we don't have credentials return next.run(request, extensions).await; }; - let url = request.url().clone(); + if matches!(auth_policy, AuthPolicy::Always) && credentials.password().is_none() { + return Err(Error::Middleware(format_err!("Missing password for {url}"))); + } let result = next.run(request, extensions).await; // Update the cache with new credentials on a successful request @@ -363,6 +375,7 @@ impl AuthMiddleware { extensions: &mut Extensions, next: Next<'_>, url: &str, + auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let credentials = Arc::new(credentials); @@ -370,7 +383,7 @@ impl AuthMiddleware { if credentials.password().is_some() { trace!("Request for {url} is already fully authenticated"); return self - .complete_request(Some(credentials), request, extensions, next) + .complete_request(Some(credentials), request, extensions, next, auth_policy) .await; } @@ -402,7 +415,7 @@ impl AuthMiddleware { }; return self - .complete_request(credentials, request, extensions, next) + .complete_request(credentials, request, extensions, next, auth_policy) .await; } diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 545977697..fba4bc355 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -10081,6 +10081,41 @@ fn add_auth_policy_always_without_credentials() -> Result<()> { Ok(()) } +/// In authentication "always", authenticated requests with a username but +/// no discoverable password will fail. +#[test] +fn add_auth_policy_always_with_username_no_password() -> 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://public@pypi.org/simple" + authenticate = "always" + default = true + "# + })?; + + uv_snapshot!(context.add().arg("anyio"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to fetch: `https://pypi.org/simple/anyio/` + Caused by: Missing password for https://pypi.org/simple/anyio/ + " + ); + Ok(()) +} + /// In authentication "never", even if the correct credentials are supplied /// in the URL, no authenticated requests will be allowed. #[test]