Skip to main content

uv_auth/
middleware.rs

1use std::sync::{Arc, LazyLock};
2
3use anyhow::{anyhow, format_err};
4use http::{Extensions, StatusCode};
5use reqwest::{Request, Response};
6use reqwest_middleware::{Error, Middleware, Next};
7use tokio::sync::Mutex;
8use tracing::{debug, trace, warn};
9
10use uv_netrc::Netrc;
11use uv_preview::{Preview, PreviewFeature};
12use uv_redacted::SafeUrl;
13use uv_static::EnvVars;
14
15use crate::providers::{
16    AzureEndpointProvider, GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider,
17};
18use crate::{
19    CredentialsCache, CredentialsPrompt, KeyringProvider,
20    cache::FetchUrl,
21    credentials::{
22        Authentication, AuthenticationError, Credentials, CredentialsFromUrlError, Username,
23    },
24    index::{AuthPolicy, Indexes},
25    realm::Realm,
26};
27use crate::{Index, TextCredentialStore};
28
29/// Cached check for whether we're running in Dependabot.
30static IS_DEPENDABOT: LazyLock<bool> =
31    LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true"));
32
33impl From<AuthenticationError> for Error {
34    fn from(err: AuthenticationError) -> Self {
35        Self::middleware(err)
36    }
37}
38
39impl From<CredentialsFromUrlError> for Error {
40    fn from(err: CredentialsFromUrlError) -> Self {
41        Self::middleware(err)
42    }
43}
44
45/// Strategy for loading netrc files.
46enum NetrcMode {
47    Automatic(LazyLock<Option<Netrc>>),
48    #[cfg(test)]
49    Enabled(Netrc),
50    #[cfg(test)]
51    Disabled,
52}
53
54impl Default for NetrcMode {
55    fn default() -> Self {
56        Self::Automatic(LazyLock::new(|| match Netrc::new() {
57            Ok(netrc) => Some(netrc),
58            Err(uv_netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
59                debug!("No netrc file found");
60                None
61            }
62            Err(err) => {
63                warn!("Error reading netrc file: {err}");
64                None
65            }
66        }))
67    }
68}
69
70impl NetrcMode {
71    /// Get the parsed netrc file if enabled.
72    fn get(&self) -> Option<&Netrc> {
73        match self {
74            Self::Automatic(lock) => lock.as_ref(),
75            #[cfg(test)]
76            Self::Enabled(netrc) => Some(netrc),
77            #[cfg(test)]
78            Self::Disabled => None,
79        }
80    }
81}
82
83/// Strategy for loading text-based credential files.
84enum TextStoreMode {
85    Automatic(tokio::sync::OnceCell<Option<TextCredentialStore>>),
86    #[cfg(test)]
87    Enabled(TextCredentialStore),
88    #[cfg(test)]
89    Disabled,
90}
91
92impl Default for TextStoreMode {
93    fn default() -> Self {
94        Self::Automatic(tokio::sync::OnceCell::new())
95    }
96}
97
98impl TextStoreMode {
99    async fn load_default_store() -> Option<TextCredentialStore> {
100        let path = TextCredentialStore::default_file()
101            .inspect_err(|err| {
102                warn!("Failed to determine credentials file path: {}", err);
103            })
104            .ok()?;
105
106        match TextCredentialStore::read(&path).await {
107            Ok((store, _lock)) => {
108                debug!("Loaded credential file `{}`", path.display());
109                Some(store)
110            }
111            Err(err)
112                if err
113                    .as_io_error()
114                    .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
115            {
116                debug!("No credentials file found at `{}`", path.display());
117                None
118            }
119            Err(err) => {
120                warn!(
121                    "Failed to load credentials from `{}`: {}",
122                    path.display(),
123                    err
124                );
125                None
126            }
127        }
128    }
129
130    /// Get the parsed credential store, if enabled.
131    async fn get(&self) -> Option<&TextCredentialStore> {
132        match self {
133            // TODO(zanieb): Reconsider this pattern. We're just mirroring the [`NetrcMode`]
134            // implementation for now.
135            Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(),
136            #[cfg(test)]
137            Self::Enabled(store) => Some(store),
138            #[cfg(test)]
139            Self::Disabled => None,
140        }
141    }
142}
143
144#[derive(Clone)]
145enum S3CredentialState {
146    /// The S3 credential state has not yet been initialized.
147    Uninitialized,
148    /// The S3 credential state has been initialized, with either a signer or `None` if
149    /// no S3 endpoint is configured.
150    Initialized(Option<Arc<Authentication>>),
151}
152
153#[derive(Clone)]
154enum GcsCredentialState {
155    /// The GCS credential state has not yet been initialized.
156    Uninitialized,
157    /// The GCS credential state has been initialized, with either a signer or `None` if
158    /// no GCS endpoint is configured.
159    Initialized(Option<Arc<Authentication>>),
160}
161
162#[derive(Clone)]
163enum AzureCredentialState {
164    /// The Azure credential state has not yet been initialized.
165    Uninitialized,
166    /// The Azure credential state has been initialized, with either a signer or `None` if
167    /// no Azure endpoint is configured.
168    Initialized(Option<Arc<Authentication>>),
169}
170
171/// A middleware that adds basic authentication to requests.
172///
173/// Uses a cache to propagate credentials from previously seen requests and
174/// fetches credentials from a netrc file, TOML file, and the keyring.
175pub struct AuthMiddleware {
176    netrc: NetrcMode,
177    text_store: TextStoreMode,
178    keyring: Option<KeyringProvider>,
179    /// Global authentication cache for a uv invocation to share credentials across uv clients.
180    cache: Arc<CredentialsCache>,
181    /// Auth policies for specific URLs.
182    indexes: Indexes,
183    /// Set all endpoints as needing authentication. We never try to send an
184    /// unauthenticated request, avoiding cloning an uncloneable request.
185    only_authenticated: bool,
186    /// Optional interactive fallback for requests that require authentication.
187    credentials_prompt: Option<CredentialsPrompt>,
188    /// Cached S3 credentials to avoid running the credential helper multiple times.
189    s3_credential_state: Mutex<S3CredentialState>,
190    /// Cached GCS credentials to avoid running the credential helper multiple times.
191    gcs_credential_state: Mutex<GcsCredentialState>,
192    /// Cached Azure credentials to avoid running the credential helper multiple times.
193    azure_credential_state: Mutex<AzureCredentialState>,
194    preview: Preview,
195}
196
197impl Default for AuthMiddleware {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203impl AuthMiddleware {
204    pub fn new() -> Self {
205        Self {
206            netrc: NetrcMode::default(),
207            text_store: TextStoreMode::default(),
208            keyring: None,
209            // TODO(konsti): There shouldn't be a credential cache without that in the initializer.
210            cache: Arc::new(CredentialsCache::default()),
211            indexes: Indexes::new(),
212            only_authenticated: false,
213            credentials_prompt: None,
214            s3_credential_state: Mutex::new(S3CredentialState::Uninitialized),
215            gcs_credential_state: Mutex::new(GcsCredentialState::Uninitialized),
216            azure_credential_state: Mutex::new(AzureCredentialState::Uninitialized),
217            preview: Preview::default(),
218        }
219    }
220
221    /// Configure the [`Netrc`] credential file to use.
222    ///
223    /// `None` disables authentication via netrc.
224    #[must_use]
225    #[cfg(test)]
226    fn with_netrc(mut self, netrc: Option<Netrc>) -> Self {
227        self.netrc = if let Some(netrc) = netrc {
228            NetrcMode::Enabled(netrc)
229        } else {
230            NetrcMode::Disabled
231        };
232        self
233    }
234
235    /// Configure the text credential store to use.
236    ///
237    /// `None` disables authentication via text store.
238    #[must_use]
239    #[cfg(test)]
240    fn with_text_store(mut self, store: Option<TextCredentialStore>) -> Self {
241        self.text_store = if let Some(store) = store {
242            TextStoreMode::Enabled(store)
243        } else {
244            TextStoreMode::Disabled
245        };
246        self
247    }
248
249    /// Configure the [`KeyringProvider`] to use.
250    #[must_use]
251    pub fn with_keyring(mut self, keyring: Option<KeyringProvider>) -> Self {
252        self.keyring = keyring;
253        self
254    }
255
256    /// Configure the [`Preview`] features to use.
257    #[must_use]
258    pub fn with_preview(mut self, preview: Preview) -> Self {
259        self.preview = preview;
260        self
261    }
262
263    /// Configure the [`CredentialsCache`] to use.
264    #[must_use]
265    #[cfg(test)]
266    fn with_cache(mut self, cache: CredentialsCache) -> Self {
267        self.cache = Arc::new(cache);
268        self
269    }
270
271    /// Configure the [`CredentialsCache`] to use from an existing [`Arc`].
272    #[must_use]
273    pub fn with_cache_arc(mut self, cache: Arc<CredentialsCache>) -> Self {
274        self.cache = cache;
275        self
276    }
277
278    /// Configure the [`AuthPolicy`]s to use for URLs.
279    #[must_use]
280    pub fn with_indexes(mut self, indexes: Indexes) -> Self {
281        self.indexes = indexes;
282        self
283    }
284
285    /// Set all endpoints as needing authentication. We never try to send an
286    /// unauthenticated request, avoiding cloning an uncloneable request.
287    #[must_use]
288    pub fn with_only_authenticated(mut self, only_authenticated: bool) -> Self {
289        self.only_authenticated = only_authenticated;
290        self
291    }
292
293    /// Configure an optional prompt for missing credentials on requests that require authentication.
294    #[must_use]
295    pub fn with_credentials_prompt(mut self, prompt: Option<CredentialsPrompt>) -> Self {
296        self.credentials_prompt = prompt;
297        self
298    }
299
300    /// Global authentication cache for a uv invocation to share credentials across uv clients.
301    fn cache(&self) -> &CredentialsCache {
302        &self.cache
303    }
304}
305
306#[async_trait::async_trait]
307impl Middleware for AuthMiddleware {
308    /// Handle authentication for a request.
309    ///
310    /// ## If the request has a username and password
311    ///
312    /// We already have a fully authenticated request and we don't need to perform a look-up.
313    ///
314    /// - Perform the request
315    /// - Add the username and password to the cache if successful
316    ///
317    /// ## If the request only has a username
318    ///
319    /// We probably need additional authentication, because a username is provided.
320    /// We'll avoid making a request we expect to fail and look for a password.
321    /// The discovered credentials must have the requested username to be used.
322    ///
323    /// - Check the cache (index URL or realm key) for a password
324    /// - Check the netrc for a password
325    /// - Check the keyring for a password
326    /// - Perform the request
327    /// - Add the username and password to the cache if successful
328    ///
329    /// ## If the request has no authentication
330    ///
331    /// We may or may not need authentication. We'll check for cached credentials for the URL,
332    /// which is relatively specific and can save us an expensive failed request. Otherwise,
333    /// we'll make the request and look for less-specific credentials on failure i.e. if the
334    /// server tells us authorization is needed. This pattern avoids attaching credentials to
335    /// requests that do not need them, which can cause some servers to deny the request.
336    ///
337    /// - Check the cache (URL key)
338    /// - Perform the request
339    /// - On 401, 403, or 404 check for authentication if there was a cache miss
340    ///     - Check the cache (index URL or realm key) for the username and password
341    ///     - Check the netrc for a username and password
342    ///     - Perform the request again if found
343    ///     - Add the username and password to the cache if successful
344    async fn handle(
345        &self,
346        mut request: Request,
347        extensions: &mut Extensions,
348        next: Next<'_>,
349    ) -> reqwest_middleware::Result<Response> {
350        // Check for credentials attached to the request already
351        let request_credentials = Credentials::from_request(&request)?.map(Authentication::from);
352
353        // In the middleware, existing credentials are already moved from the URL
354        // to the headers so for display purposes we restore some information
355        let url = tracing_url(&request, request_credentials.as_ref());
356        let request_url = SafeUrl::ref_cast(request.url());
357        let index = self.indexes.index_for(request_url);
358        let auth_policy = self.indexes.auth_policy_for(request_url);
359        trace!("Handling request for `{url}` with authentication policy {auth_policy}");
360
361        let credentials: Option<Arc<Authentication>> = if matches!(auth_policy, AuthPolicy::Never) {
362            None
363        } else {
364            if let Some(request_credentials) = request_credentials {
365                return self
366                    .complete_request_with_request_credentials(
367                        request_credentials,
368                        request,
369                        extensions,
370                        next,
371                        &url,
372                        index,
373                        auth_policy,
374                    )
375                    .await;
376            }
377
378            // We have no credentials
379            trace!("Request for `{url}` is unauthenticated, checking cache");
380
381            // Check the cache for a URL match first. This can save us from
382            // making a failing request
383            let credentials = self
384                .cache()
385                .get_url(SafeUrl::ref_cast(request.url()), &Username::none());
386            if let Some(credentials) = credentials.as_ref() {
387                request = credentials.authenticate(request).await?;
388
389                // If it's fully authenticated, finish the request
390                if credentials.is_authenticated() {
391                    trace!("Request for `{url}` is fully authenticated");
392                    return self
393                        .complete_request(None, request, extensions, next, auth_policy)
394                        .await;
395                }
396
397                // If we just found a username, we'll make the request then look for password elsewhere
398                // if it fails
399                trace!("Found username for `{url}` in cache, attempting request");
400            }
401            credentials
402        };
403        let attempt_has_username = credentials
404            .as_ref()
405            .is_some_and(|credentials| credentials.username().is_some());
406
407        let must_authenticate = self.only_authenticated
408            || (matches!(auth_policy, AuthPolicy::Always)
409                // Dependabot intercepts HTTP requests and injects credentials, which means that we
410                // cannot eagerly enforce an `AuthPolicy` as we don't know whether credentials will be
411                // added outside of uv.
412                && !*IS_DEPENDABOT);
413
414        let (mut retry_request, response) = if !must_authenticate {
415            let url = tracing_url(&request, credentials.as_deref());
416            if credentials.is_none() {
417                trace!("Attempting unauthenticated request for `{url}`");
418            } else {
419                trace!("Attempting partially authenticated request for `{url}`");
420            }
421
422            // <https://github.com/TrueLayer/reqwest-middleware/blob/abdf1844c37092d323683c2396b7eefda1418d3c/reqwest-retry/src/middleware.rs#L141-L149>
423            // Clone the request so we can retry it on authentication failure
424            let retry_request = request.try_clone().ok_or_else(|| {
425                Error::Middleware(anyhow!(
426                    "Request object is not cloneable. Are you passing a streaming body?"
427                        .to_string()
428                ))
429            })?;
430
431            let response = next.clone().run(request, extensions).await?;
432
433            // If we don't fail with authorization related codes or
434            // authentication policy is Never, return the response.
435            if !matches!(
436                response.status(),
437                StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED
438            ) || matches!(auth_policy, AuthPolicy::Never)
439            {
440                return Ok(response);
441            }
442
443            // Otherwise, search for credentials
444            trace!(
445                "Request for `{url}` failed with {}, checking for credentials",
446                response.status()
447            );
448
449            (retry_request, Some(response))
450        } else {
451            // For endpoints where we require the user to provide credentials, we don't try the
452            // unauthenticated request first.
453            trace!("Checking for credentials for `{url}`");
454            (request, None)
455        };
456        let retry_request_url = SafeUrl::ref_cast(retry_request.url());
457
458        let username = credentials
459            .as_ref()
460            .map(|credentials| credentials.to_username())
461            .unwrap_or(Username::none());
462        let credentials = if let Some(index) = index {
463            self.cache().get_url(&index.url, &username).or_else(|| {
464                self.cache()
465                    .get_realm(Realm::from(retry_request_url), username)
466            })
467        } else {
468            // Since there is no known index for this URL, check if there are credentials in
469            // the realm-level cache.
470            self.cache()
471                .get_realm(Realm::from(retry_request_url), username)
472        }
473        .or(credentials);
474
475        if let Some(credentials) = credentials.as_ref() {
476            if credentials.is_authenticated() {
477                trace!("Retrying request for `{url}` with credentials from cache {credentials:?}");
478                retry_request = credentials.authenticate(retry_request).await?;
479                return self
480                    .complete_request(None, retry_request, extensions, next, auth_policy)
481                    .await;
482            }
483        }
484
485        // Then, fetch from external services.
486        // Here, we use the username from the cache if present.
487        let mut fetched_credentials = self
488            .fetch_credentials(
489                credentials.as_deref(),
490                retry_request_url,
491                index,
492                auth_policy,
493            )
494            .await?;
495
496        // If no complete credentials were found, prompt before sending the request.
497        if must_authenticate
498            && !matches!(auth_policy, AuthPolicy::Never)
499            && !fetched_credentials
500                .as_ref()
501                .is_some_and(|credentials| credentials.is_authenticated())
502            && let Some(prompt) = &self.credentials_prompt
503            && let Some(credentials) = prompt.prompt().await.map_err(Error::middleware)?
504        {
505            fetched_credentials = Some(Arc::new(Authentication::from(credentials)));
506        }
507
508        if let Some(credentials) = fetched_credentials {
509            retry_request = credentials.authenticate(retry_request).await?;
510            trace!("Retrying request for `{url}` with {credentials:?}");
511            return self
512                .complete_request(
513                    Some(credentials),
514                    retry_request,
515                    extensions,
516                    next,
517                    auth_policy,
518                )
519                .await;
520        }
521
522        if let Some(credentials) = credentials.as_ref() {
523            if !attempt_has_username {
524                trace!("Retrying request for `{url}` with username from cache {credentials:?}");
525                retry_request = credentials.authenticate(retry_request).await?;
526                return self
527                    .complete_request(None, retry_request, extensions, next, auth_policy)
528                    .await;
529            }
530        }
531
532        if let Some(response) = response {
533            Ok(response)
534        } else {
535            Err(Error::Middleware(format_err!(
536                "Missing credentials for: {url}"
537            )))
538        }
539    }
540}
541
542impl AuthMiddleware {
543    /// Run a request to completion.
544    ///
545    /// If credentials are present, insert them into the cache on success.
546    async fn complete_request(
547        &self,
548        credentials: Option<Arc<Authentication>>,
549        request: Request,
550        extensions: &mut Extensions,
551        next: Next<'_>,
552        auth_policy: AuthPolicy,
553    ) -> reqwest_middleware::Result<Response> {
554        let Some(credentials) = credentials else {
555            // Nothing to insert into the cache if we don't have credentials
556            return next.run(request, extensions).await;
557        };
558        let url = SafeUrl::from_url(request.url().clone());
559        if matches!(auth_policy, AuthPolicy::Always) && !credentials.is_authenticated() {
560            return Err(Error::Middleware(format_err!(
561                "Incomplete credentials for `{url}`"
562            )));
563        }
564        let result = next.run(request, extensions).await;
565
566        // Update the cache with new credentials on a successful request
567        if result
568            .as_ref()
569            .is_ok_and(|response| response.error_for_status_ref().is_ok())
570        {
571            // TODO(zanieb): Consider also updating the system keyring after successful use
572            trace!("Updating cached credentials for `{url}` to {credentials:?}");
573            self.cache().insert(&url, credentials);
574        }
575
576        result
577    }
578
579    /// Use known request credentials to complete the request.
580    async fn complete_request_with_request_credentials(
581        &self,
582        credentials: Authentication,
583        mut request: Request,
584        extensions: &mut Extensions,
585        next: Next<'_>,
586        url: &SafeUrl,
587        index: Option<&Index>,
588        auth_policy: AuthPolicy,
589    ) -> reqwest_middleware::Result<Response> {
590        let credentials = Arc::new(credentials);
591
592        // If the request already contains complete authentication, send it and cache it.
593        if credentials.is_authenticated() {
594            trace!("Request for `{url}` already contains complete authentication");
595            return self
596                .complete_request(Some(credentials), request, extensions, next, auth_policy)
597                .await;
598        }
599
600        trace!("Request for `{url}` is missing a password, looking for credentials");
601
602        // There's just a username, try to find a password.
603        // If we have an index, check the cache for that URL. Otherwise,
604        // check for the realm.
605        let maybe_cached_credentials = if let Some(index) = index {
606            self.cache()
607                .get_url(&index.url, credentials.as_username().as_ref())
608                .or_else(|| {
609                    self.cache()
610                        .get_url(&index.root_url, credentials.as_username().as_ref())
611                })
612        } else {
613            self.cache().get_realm(
614                Realm::from(SafeUrl::ref_cast(request.url())),
615                credentials.to_username(),
616            )
617        };
618        if let Some(credentials) = maybe_cached_credentials {
619            request = credentials.authenticate(request).await?;
620            // Do not insert already-cached credentials
621            let credentials = None;
622            return self
623                .complete_request(credentials, request, extensions, next, auth_policy)
624                .await;
625        }
626
627        let credentials = if let Some(credentials) = self.cache().get_url(
628            SafeUrl::ref_cast(request.url()),
629            credentials.as_username().as_ref(),
630        ) {
631            request = credentials.authenticate(request).await?;
632            // Do not insert already-cached credentials
633            None
634        } else if let Some(credentials) = self
635            .fetch_credentials(
636                Some(&credentials),
637                SafeUrl::ref_cast(request.url()),
638                index,
639                auth_policy,
640            )
641            .await?
642        {
643            request = credentials.authenticate(request).await?;
644            Some(credentials)
645        } else if index.is_some() {
646            // If this is a known index, we fall back to checking for the realm.
647            if let Some(credentials) = self.cache().get_realm(
648                Realm::from(SafeUrl::ref_cast(request.url())),
649                credentials.to_username(),
650            ) {
651                request = credentials.authenticate(request).await?;
652                Some(credentials)
653            } else {
654                Some(credentials)
655            }
656        } else {
657            // If we don't find a password, we'll still attempt the request with the existing credentials
658            Some(credentials)
659        };
660
661        self.complete_request(credentials, request, extensions, next, auth_policy)
662            .await
663    }
664
665    /// Fetch credentials for a URL.
666    ///
667    /// Supports netrc file and keyring lookups.
668    async fn fetch_credentials(
669        &self,
670        credentials: Option<&Authentication>,
671        url: &SafeUrl,
672        index: Option<&Index>,
673        auth_policy: AuthPolicy,
674    ) -> reqwest_middleware::Result<Option<Arc<Authentication>>> {
675        let is_s3_endpoint =
676            S3EndpointProvider::is_s3_endpoint(url, self.preview).map_err(Error::Middleware)?;
677        let is_gcs_endpoint =
678            GcsEndpointProvider::is_gcs_endpoint(url, self.preview).map_err(Error::Middleware)?;
679        let is_azure_endpoint = AzureEndpointProvider::is_azure_endpoint(url, self.preview)
680            .map_err(Error::Middleware)?;
681        let username = Username::from(
682            credentials.map(|credentials| credentials.username().unwrap_or_default().to_string()),
683        );
684
685        // Fetches can be expensive, so we will only run them _once_ per realm or index URL and username combination
686        // All other requests for the same realm or index URL will wait until the first one completes
687        let key = if let Some(index) = index {
688            (FetchUrl::Index(index.url.clone()), username)
689        } else {
690            (FetchUrl::Realm(Realm::from(url)), username)
691        };
692        if let Some(credentials) = self.cache().fetches.register_or_wait(&key).await {
693            if credentials.is_some() {
694                trace!("Using credentials from previous fetch for {}", key.0);
695            } else {
696                trace!(
697                    "Skipping fetch of credentials for {}, previous attempt failed",
698                    key.0
699                );
700            }
701
702            return Ok(credentials);
703        }
704
705        // Support for known providers, like Hugging Face and S3.
706        if let Some(credentials) = HuggingFaceProvider::credentials_for(url)
707            .map(Authentication::from)
708            .map(Arc::new)
709        {
710            debug!("Found Hugging Face credentials for `{url}`");
711            self.cache().fetches.done(key, Some(credentials.clone()));
712            return Ok(Some(credentials));
713        }
714
715        if is_s3_endpoint {
716            let mut s3_state = self.s3_credential_state.lock().await;
717
718            // If the S3 credential state is uninitialized, initialize it.
719            let credentials = match &*s3_state {
720                S3CredentialState::Uninitialized => {
721                    trace!("Initializing S3 credentials for `{url}`");
722                    let signer = S3EndpointProvider::create_signer();
723                    let credentials = Arc::new(Authentication::from(signer));
724                    *s3_state = S3CredentialState::Initialized(Some(credentials.clone()));
725                    Some(credentials)
726                }
727                S3CredentialState::Initialized(credentials) => credentials.clone(),
728            };
729
730            if let Some(credentials) = credentials {
731                debug!("Found S3 credentials for `{url}`");
732                self.cache().fetches.done(key, Some(credentials.clone()));
733                return Ok(Some(credentials));
734            }
735        }
736
737        if is_gcs_endpoint {
738            let mut gcs_state = self.gcs_credential_state.lock().await;
739
740            // If the GCS credential state is uninitialized, initialize it.
741            let credentials = match &*gcs_state {
742                GcsCredentialState::Uninitialized => {
743                    trace!("Initializing GCS credentials for `{url}`");
744                    let signer = GcsEndpointProvider::create_signer();
745                    let credentials = Arc::new(Authentication::from(signer));
746                    *gcs_state = GcsCredentialState::Initialized(Some(credentials.clone()));
747                    Some(credentials)
748                }
749                GcsCredentialState::Initialized(credentials) => credentials.clone(),
750            };
751
752            if let Some(credentials) = credentials {
753                debug!("Found GCS credentials for `{url}`");
754                self.cache().fetches.done(key, Some(credentials.clone()));
755                return Ok(Some(credentials));
756            }
757        }
758
759        if is_azure_endpoint {
760            let mut azure_state = self.azure_credential_state.lock().await;
761
762            // If the Azure credential state is uninitialized, initialize it.
763            let credentials = match &*azure_state {
764                AzureCredentialState::Uninitialized => {
765                    trace!("Initializing Azure credentials for `{url}`");
766                    let signer = AzureEndpointProvider::create_signer();
767                    let credentials = Arc::new(Authentication::from(signer));
768                    *azure_state = AzureCredentialState::Initialized(Some(credentials.clone()));
769                    Some(credentials)
770                }
771                AzureCredentialState::Initialized(credentials) => credentials.clone(),
772            };
773
774            if let Some(credentials) = credentials {
775                debug!("Found Azure credentials for `{url}`");
776                self.cache().fetches.done(key, Some(credentials.clone()));
777                return Ok(Some(credentials));
778            }
779        }
780
781        // Netrc support based on: <https://github.com/gribouille/netrc>.
782        let credentials = if let Some(credentials) = self.netrc.get().and_then(|netrc| {
783            debug!("Checking netrc for credentials for `{url}`");
784            Credentials::from_netrc(
785                netrc,
786                url,
787                credentials
788                    .as_ref()
789                    .and_then(|credentials| credentials.username()),
790            )
791        }) {
792            debug!("Found credentials in netrc file for `{url}`");
793            Some(credentials)
794
795        // Text credential store support.
796        } else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| {
797            debug!("Checking text store for credentials for `{url}`");
798            match text_store.get_credentials(
799                url,
800                credentials
801                    .as_ref()
802                    .and_then(|credentials| credentials.username()),
803            ) {
804                Ok(credentials) => credentials.cloned(),
805                Err(err) => {
806                    debug!("Failed to get credentials from text store: {err}");
807                    None
808                }
809            }
810        }) {
811            debug!("Found credentials in plaintext store for `{url}`");
812            Some(credentials)
813        } else if let Some(credentials) = {
814            if self.preview.is_enabled(PreviewFeature::NativeAuth) {
815                let native_store = KeyringProvider::native();
816                let username = credentials.and_then(|credentials| credentials.username());
817                let display_username = if let Some(username) = username {
818                    format!("{username}@")
819                } else {
820                    String::new()
821                };
822                if let Some(index) = index {
823                    // N.B. The native store performs an exact look up right now, so we use the root
824                    // URL of the index instead of relying on prefix-matching.
825                    debug!(
826                        "Checking native store for credentials for index URL {}{}",
827                        display_username, index.root_url
828                    );
829                    native_store.fetch(&index.root_url, username).await
830                } else {
831                    debug!(
832                        "Checking native store for credentials for URL `{}{}`",
833                        display_username, url
834                    );
835                    native_store.fetch(url, username).await
836                }
837                // TODO(zanieb): We should have a realm fallback here too
838            } else {
839                None
840            }
841        } {
842            debug!("Found credentials in native store for `{url}`");
843            Some(credentials)
844        // N.B. The keyring provider performs lookups for the exact URL then falls back to the host.
845        //      But, in the absence of an index URL, we cache the result per realm. So in that case,
846        //      if a keyring implementation returns different credentials for different URLs in the
847        //      same realm we will use the wrong credentials.
848        } else if let Some(credentials) = match self.keyring {
849            Some(ref keyring) => {
850                // The subprocess keyring provider is _slow_ so we do not perform fetches for all
851                // URLs; instead, we fetch if there's a username or if the user has requested to
852                // always authenticate.
853                if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
854                    if let Some(index) = index {
855                        debug!(
856                            "Checking keyring for credentials for index URL `{}@{}`",
857                            username, index.url
858                        );
859                        keyring.fetch(&index.url, Some(username)).await
860                    } else {
861                        debug!(
862                            "Checking keyring for credentials for full URL `{}@{}`",
863                            username, url
864                        );
865                        keyring.fetch(url, Some(username)).await
866                    }
867                } else if matches!(auth_policy, AuthPolicy::Always) {
868                    if let Some(index) = index {
869                        debug!(
870                            "Checking keyring for credentials for index URL `{}` without username due to `authenticate = always`",
871                            index.url
872                        );
873                        keyring.fetch(&index.url, None).await
874                    } else {
875                        None
876                    }
877                } else {
878                    debug!(
879                        "Skipping keyring fetch for `{url}` without username; use `authenticate = always` to force"
880                    );
881                    None
882                }
883            }
884            None => None,
885        } {
886            debug!("Found credentials in keyring for `{url}`");
887            Some(credentials)
888        } else {
889            None
890        };
891
892        let credentials = credentials.map(Authentication::from).map(Arc::new);
893
894        // Register the fetch for this key
895        self.cache().fetches.done(key, credentials.clone());
896
897        Ok(credentials)
898    }
899}
900
901fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> SafeUrl {
902    let mut url = SafeUrl::from_url(request.url().clone());
903    if let Some(Authentication::Credentials(creds)) = credentials {
904        if let Some(username) = creds.username() {
905            let _ = url.set_username(username);
906        }
907        if let Some(password) = creds.password() {
908            let _ = url.set_password(Some(password));
909        }
910    }
911    url
912}
913
914#[cfg(test)]
915mod tests {
916    use std::assert_matches;
917    use std::io::{self, Write};
918    use std::sync::atomic::{AtomicUsize, Ordering};
919
920    use http::Method;
921    use reqwest::Client;
922    use tempfile::NamedTempFile;
923    use test_log::test;
924    use wiremock::matchers::{basic_auth, method, path_regex};
925    use wiremock::{Mock, MockServer, ResponseTemplate};
926
927    use crate::credentials::Password;
928    use crate::{CredentialsPromptError, Index};
929
930    use super::*;
931
932    type Error = Box<dyn std::error::Error>;
933
934    static RETRY_PROMPTS: AtomicUsize = AtomicUsize::new(0);
935
936    async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
937        let server = MockServer::start().await;
938
939        Mock::given(method("GET"))
940            .and(basic_auth(username, password))
941            .respond_with(ResponseTemplate::new(200))
942            .mount(&server)
943            .await;
944
945        Mock::given(method("GET"))
946            .respond_with(ResponseTemplate::new(401))
947            .mount(&server)
948            .await;
949
950        server
951    }
952
953    fn test_client_builder() -> reqwest_middleware::ClientBuilder {
954        reqwest_middleware::ClientBuilder::new(
955            Client::builder()
956                .build()
957                .expect("Reqwest client should build"),
958        )
959    }
960
961    #[test(tokio::test)]
962    async fn test_no_credentials() -> Result<(), Error> {
963        let server = start_test_server("user", "password").await;
964        let client = test_client_builder()
965            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
966            .build();
967
968        assert_eq!(
969            client
970                .get(format!("{}/foo", server.uri()))
971                .send()
972                .await?
973                .status(),
974            401
975        );
976
977        assert_eq!(
978            client
979                .get(format!("{}/bar", server.uri()))
980                .send()
981                .await?
982                .status(),
983            401
984        );
985
986        Ok(())
987    }
988
989    /// Prompted credentials survive a failed request without prompting again on retry.
990    #[test(tokio::test)]
991    async fn test_credentials_prompt_retry() -> Result<(), Error> {
992        let server = start_test_server("user", "password").await;
993        let cache = CredentialsCache::new();
994        // A username discovered at another path in the same realm still needs a password.
995        cache.store_credentials(
996            &SafeUrl::parse(&format!("{}/read/simple/", server.uri()))?,
997            Credentials::basic(Some("reader".to_string()), None),
998        );
999        Mock::given(method("GET"))
1000            .and(path_regex("/unavailable"))
1001            .and(basic_auth("user", "password"))
1002            .respond_with(ResponseTemplate::new(503))
1003            .with_priority(1)
1004            .expect(1)
1005            .mount(&server)
1006            .await;
1007
1008        let client = test_client_builder()
1009            .with(
1010                AuthMiddleware::new()
1011                    .with_netrc(None)
1012                    .with_text_store(None)
1013                    .with_cache(cache)
1014                    .with_only_authenticated(true)
1015                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| {
1016                        RETRY_PROMPTS.fetch_add(1, Ordering::SeqCst);
1017                        Ok(Some(Credentials::basic(
1018                            Some("user".to_string()),
1019                            Some("password".to_string()),
1020                        )))
1021                    }))),
1022            )
1023            .build();
1024
1025        assert_eq!(
1026            client
1027                .get(format!("{}/unavailable", server.uri()))
1028                .send()
1029                .await?
1030                .status(),
1031            503
1032        );
1033        assert_eq!(client.get(server.uri()).send().await?.status(), 200);
1034        assert_eq!(RETRY_PROMPTS.load(Ordering::SeqCst), 1);
1035
1036        Ok(())
1037    }
1038
1039    /// Explicit credentials must not be replaced by interactive input, even when incomplete.
1040    #[test(tokio::test)]
1041    async fn test_credentials_prompt_explicit_credentials() -> Result<(), Error> {
1042        let server = start_test_server("user", "password").await;
1043        let client = test_client_builder()
1044            .with(
1045                AuthMiddleware::new()
1046                    .with_netrc(None)
1047                    .with_text_store(None)
1048                    .with_only_authenticated(true)
1049                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| {
1050                        Err(CredentialsPromptError::ReadUsername(io::Error::other(
1051                            "Should not prompt for explicit credentials",
1052                        )))
1053                    }))),
1054            )
1055            .build();
1056
1057        assert_eq!(
1058            client
1059                .get(server.uri())
1060                .basic_auth("user", Some("invalid"))
1061                .send()
1062                .await?
1063                .status(),
1064            401
1065        );
1066        assert_eq!(
1067            client
1068                .get(server.uri())
1069                .basic_auth("user", None::<&str>)
1070                .send()
1071                .await?
1072                .status(),
1073            401
1074        );
1075
1076        Ok(())
1077    }
1078
1079    #[test(tokio::test)]
1080    async fn test_credentials_prompt_unavailable() -> Result<(), Error> {
1081        let server = start_test_server("user", "password").await;
1082        let client = test_client_builder()
1083            .with(
1084                AuthMiddleware::new()
1085                    .with_netrc(None)
1086                    .with_text_store(None)
1087                    .with_only_authenticated(true)
1088                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| Ok(None)))),
1089            )
1090            .build();
1091
1092        let err = client
1093            .get(server.uri())
1094            .send()
1095            .await
1096            .expect_err("A request without credentials must not be sent");
1097        insta::with_settings!({filters => [(r"http://127.0.0.1:\d+", "http://[LOCALHOST]")]}, {
1098            insta::assert_snapshot!(err, @"Missing credentials for: http://[LOCALHOST]/");
1099        });
1100        assert!(
1101            server
1102                .received_requests()
1103                .await
1104                .expect("Recorded requests")
1105                .is_empty()
1106        );
1107
1108        Ok(())
1109    }
1110
1111    #[test(tokio::test)]
1112    async fn test_credentials_prompt_error() -> Result<(), Error> {
1113        let server = start_test_server("user", "password").await;
1114        let client = test_client_builder()
1115            .with(
1116                AuthMiddleware::new()
1117                    .with_netrc(None)
1118                    .with_text_store(None)
1119                    .with_only_authenticated(true)
1120                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| {
1121                        Err(CredentialsPromptError::ReadUsername(io::Error::other(
1122                            "terminal input failed",
1123                        )))
1124                    }))),
1125            )
1126            .build();
1127
1128        let err = client
1129            .get(server.uri())
1130            .send()
1131            .await
1132            .expect_err("A prompt failure must prevent the request");
1133        insta::assert_snapshot!(format!("{:#}", anyhow::Error::new(err)), @"Failed to read username: terminal input failed");
1134        assert!(
1135            server
1136                .received_requests()
1137                .await
1138                .expect("Recorded requests")
1139                .is_empty()
1140        );
1141
1142        Ok(())
1143    }
1144
1145    #[test(tokio::test)]
1146    async fn test_credentials_prompt_auth_policy_never() -> Result<(), Error> {
1147        let server = start_test_server("user", "password").await;
1148        let indexes = indexes_for(&SafeUrl::parse(&server.uri())?, AuthPolicy::Never);
1149        let client = test_client_builder()
1150            .with(
1151                AuthMiddleware::new()
1152                    .with_netrc(None)
1153                    .with_text_store(None)
1154                    .with_indexes(indexes)
1155                    .with_only_authenticated(true)
1156                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| {
1157                        Err(CredentialsPromptError::ReadUsername(io::Error::other(
1158                            "Should not prompt when authentication is disabled",
1159                        )))
1160                    }))),
1161            )
1162            .build();
1163
1164        let err = client
1165            .get(server.uri())
1166            .send()
1167            .await
1168            .expect_err("A request without credentials must not be sent");
1169        insta::with_settings!({filters => [(r"http://127.0.0.1:\d+", "http://[LOCALHOST]")]}, {
1170            insta::assert_snapshot!(err, @"Missing credentials for: http://[LOCALHOST]/");
1171        });
1172        assert!(
1173            server
1174                .received_requests()
1175                .await
1176                .expect("Recorded requests")
1177                .is_empty()
1178        );
1179
1180        Ok(())
1181    }
1182
1183    /// Without seeding the cache, authenticated requests are not cached
1184    #[test(tokio::test)]
1185    async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
1186        let username = "user";
1187        let password = "password";
1188
1189        let server = start_test_server(username, password).await;
1190        let client = test_client_builder()
1191            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1192            .build();
1193
1194        let base_url = SafeUrl::parse(&server.uri())?;
1195
1196        let mut url = base_url.clone();
1197        url.set_username(username).unwrap();
1198        url.set_password(Some(password)).unwrap();
1199        assert_eq!(
1200            client.get(url.into_unsafe_url()).send().await?.status(),
1201            200
1202        );
1203
1204        // Works for a URL without credentials now
1205        assert_eq!(
1206            client.get(server.uri()).send().await?.status(),
1207            200,
1208            "Subsequent requests should not require credentials"
1209        );
1210
1211        assert_eq!(
1212            client
1213                .get(format!("{}/foo", server.uri()))
1214                .send()
1215                .await?
1216                .status(),
1217            200,
1218            "Requests can be to different paths in the same realm"
1219        );
1220
1221        let mut url = base_url.clone();
1222        url.set_username(username).unwrap();
1223        url.set_password(Some("invalid")).unwrap();
1224        assert_eq!(
1225            client.get(url.into_unsafe_url()).send().await?.status(),
1226            401,
1227            "Credentials in the URL should take precedence and fail"
1228        );
1229
1230        Ok(())
1231    }
1232
1233    #[test(tokio::test)]
1234    async fn test_credentials_in_url_seed() -> Result<(), Error> {
1235        let username = "user";
1236        let password = "password";
1237
1238        let server = start_test_server(username, password).await;
1239        let base_url = SafeUrl::parse(&server.uri())?;
1240        let cache = CredentialsCache::new();
1241        cache.insert(
1242            &base_url,
1243            Arc::new(Authentication::from(Credentials::basic(
1244                Some(username.to_string()),
1245                Some(password.to_string()),
1246            ))),
1247        );
1248
1249        let client = test_client_builder()
1250            .with(AuthMiddleware::new().with_cache(cache))
1251            .build();
1252
1253        let mut url = base_url.clone();
1254        url.set_username(username).unwrap();
1255        url.set_password(Some(password)).unwrap();
1256        assert_eq!(
1257            client.get(url.into_unsafe_url()).send().await?.status(),
1258            200
1259        );
1260
1261        // Works for a URL without credentials too
1262        assert_eq!(
1263            client.get(server.uri()).send().await?.status(),
1264            200,
1265            "Requests should not require credentials"
1266        );
1267
1268        assert_eq!(
1269            client
1270                .get(format!("{}/foo", server.uri()))
1271                .send()
1272                .await?
1273                .status(),
1274            200,
1275            "Requests can be to different paths in the same realm"
1276        );
1277
1278        let mut url = base_url.clone();
1279        url.set_username(username).unwrap();
1280        url.set_password(Some("invalid")).unwrap();
1281        assert_eq!(
1282            client.get(url.into_unsafe_url()).send().await?.status(),
1283            401,
1284            "Credentials in the URL should take precedence and fail"
1285        );
1286
1287        Ok(())
1288    }
1289
1290    #[test(tokio::test)]
1291    async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1292        let username = "user";
1293        let password = "";
1294
1295        let server = start_test_server(username, password).await;
1296        let base_url = SafeUrl::parse(&server.uri())?;
1297        let cache = CredentialsCache::new();
1298        cache.insert(
1299            &base_url,
1300            Arc::new(Authentication::from(Credentials::basic(
1301                Some(username.to_string()),
1302                None,
1303            ))),
1304        );
1305
1306        let client = test_client_builder()
1307            .with(AuthMiddleware::new().with_cache(cache))
1308            .build();
1309
1310        let mut url = base_url.clone();
1311        url.set_username(username).unwrap();
1312        url.set_password(None).unwrap();
1313        assert_eq!(
1314            client.get(url.into_unsafe_url()).send().await?.status(),
1315            200
1316        );
1317
1318        // Works for a URL without credentials too
1319        assert_eq!(
1320            client.get(server.uri()).send().await?.status(),
1321            200,
1322            "Requests should not require credentials"
1323        );
1324
1325        assert_eq!(
1326            client
1327                .get(format!("{}/foo", server.uri()))
1328                .send()
1329                .await?
1330                .status(),
1331            200,
1332            "Requests can be to different paths in the same realm"
1333        );
1334
1335        let mut url = base_url.clone();
1336        url.set_username(username).unwrap();
1337        url.set_password(Some("invalid")).unwrap();
1338        assert_eq!(
1339            client.get(url.into_unsafe_url()).send().await?.status(),
1340            401,
1341            "Credentials in the URL should take precedence and fail"
1342        );
1343
1344        assert_eq!(
1345            client.get(server.uri()).send().await?.status(),
1346            200,
1347            "Subsequent requests should not use the invalid credentials"
1348        );
1349
1350        Ok(())
1351    }
1352
1353    #[test(tokio::test)]
1354    async fn test_netrc_file_default_host() -> Result<(), Error> {
1355        let username = "user";
1356        let password = "password";
1357
1358        let mut netrc_file = NamedTempFile::new()?;
1359        writeln!(netrc_file, "default login {username} password {password}")?;
1360
1361        let server = start_test_server(username, password).await;
1362        let client = test_client_builder()
1363            .with(
1364                AuthMiddleware::new()
1365                    .with_cache(CredentialsCache::new())
1366                    .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1367            )
1368            .build();
1369
1370        assert_eq!(
1371            client.get(server.uri()).send().await?.status(),
1372            200,
1373            "Credentials should be pulled from the netrc file"
1374        );
1375
1376        let mut url = SafeUrl::parse(&server.uri())?;
1377        url.set_username(username).unwrap();
1378        url.set_password(Some("invalid")).unwrap();
1379        assert_eq!(
1380            client.get(url.into_unsafe_url()).send().await?.status(),
1381            401,
1382            "Credentials in the URL should take precedence and fail"
1383        );
1384
1385        assert_eq!(
1386            client.get(server.uri()).send().await?.status(),
1387            200,
1388            "Subsequent requests should not use the invalid credentials"
1389        );
1390
1391        Ok(())
1392    }
1393
1394    #[test(tokio::test)]
1395    async fn test_netrc_file_matching_host() -> Result<(), Error> {
1396        let username = "user";
1397        let password = "password";
1398        let server = start_test_server(username, password).await;
1399        let base_url = SafeUrl::parse(&server.uri())?;
1400
1401        let mut netrc_file = NamedTempFile::new()?;
1402        writeln!(
1403            netrc_file,
1404            r"machine {} login {username} password {password}",
1405            base_url.host_str().unwrap()
1406        )?;
1407
1408        let client = test_client_builder()
1409            .with(
1410                AuthMiddleware::new()
1411                    .with_cache(CredentialsCache::new())
1412                    .with_netrc(Some(
1413                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1414                    )),
1415            )
1416            .build();
1417
1418        assert_eq!(
1419            client.get(server.uri()).send().await?.status(),
1420            200,
1421            "Credentials should be pulled from the netrc file"
1422        );
1423
1424        let mut url = base_url.clone();
1425        url.set_username(username).unwrap();
1426        url.set_password(Some("invalid")).unwrap();
1427        assert_eq!(
1428            client.get(url.into_unsafe_url()).send().await?.status(),
1429            401,
1430            "Credentials in the URL should take precedence and fail"
1431        );
1432
1433        assert_eq!(
1434            client.get(server.uri()).send().await?.status(),
1435            200,
1436            "Subsequent requests should not use the invalid credentials"
1437        );
1438
1439        Ok(())
1440    }
1441
1442    #[test(tokio::test)]
1443    async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1444        let username = "user";
1445        let password = "password";
1446        let server = start_test_server(username, password).await;
1447
1448        let mut netrc_file = NamedTempFile::new()?;
1449        writeln!(
1450            netrc_file,
1451            r"machine example.com login {username} password {password}",
1452        )?;
1453
1454        let client = test_client_builder()
1455            .with(
1456                AuthMiddleware::new()
1457                    .with_cache(CredentialsCache::new())
1458                    .with_netrc(Some(
1459                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1460                    )),
1461            )
1462            .build();
1463
1464        assert_eq!(
1465            client.get(server.uri()).send().await?.status(),
1466            401,
1467            "Credentials should not be pulled from the netrc file due to host mismatch"
1468        );
1469
1470        let mut url = SafeUrl::parse(&server.uri())?;
1471        url.set_username(username).unwrap();
1472        url.set_password(Some(password)).unwrap();
1473        assert_eq!(
1474            client.get(url.into_unsafe_url()).send().await?.status(),
1475            200,
1476            "Credentials in the URL should still work"
1477        );
1478
1479        Ok(())
1480    }
1481
1482    #[test(tokio::test)]
1483    async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1484        let username = "user";
1485        let password = "password";
1486        let server = start_test_server(username, password).await;
1487        let base_url = SafeUrl::parse(&server.uri())?;
1488
1489        let mut netrc_file = NamedTempFile::new()?;
1490        writeln!(
1491            netrc_file,
1492            r"machine {} login {username} password {password}",
1493            base_url.host_str().unwrap()
1494        )?;
1495
1496        let client = test_client_builder()
1497            .with(
1498                AuthMiddleware::new()
1499                    .with_cache(CredentialsCache::new())
1500                    .with_netrc(Some(
1501                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1502                    )),
1503            )
1504            .build();
1505
1506        let mut url = base_url.clone();
1507        url.set_username("other-user").unwrap();
1508        assert_eq!(
1509            client.get(url.into_unsafe_url()).send().await?.status(),
1510            401,
1511            "The netrc password should not be used due to a username mismatch"
1512        );
1513
1514        let mut url = base_url.clone();
1515        url.set_username("user").unwrap();
1516        assert_eq!(
1517            client.get(url.into_unsafe_url()).send().await?.status(),
1518            200,
1519            "The netrc password should be used for a matching user"
1520        );
1521
1522        Ok(())
1523    }
1524
1525    #[test(tokio::test)]
1526    async fn test_keyring() -> Result<(), Error> {
1527        let username = "user";
1528        let password = "password";
1529        let server = start_test_server(username, password).await;
1530        let base_url = SafeUrl::parse(&server.uri())?;
1531
1532        let client = test_client_builder()
1533            .with(
1534                AuthMiddleware::new()
1535                    .with_cache(CredentialsCache::new())
1536                    .with_keyring(Some(KeyringProvider::dummy([(
1537                        format!(
1538                            "{}:{}",
1539                            base_url.host_str().unwrap(),
1540                            base_url.port().unwrap()
1541                        ),
1542                        username,
1543                        password,
1544                    )]))),
1545            )
1546            .build();
1547
1548        assert_eq!(
1549            client.get(server.uri()).send().await?.status(),
1550            401,
1551            "Credentials are not pulled from the keyring without a username"
1552        );
1553
1554        let mut url = base_url.clone();
1555        url.set_username(username).unwrap();
1556        assert_eq!(
1557            client.get(url.into_unsafe_url()).send().await?.status(),
1558            200,
1559            "Credentials for the username should be pulled from the keyring"
1560        );
1561
1562        let mut url = base_url.clone();
1563        url.set_username(username).unwrap();
1564        url.set_password(Some("invalid")).unwrap();
1565        assert_eq!(
1566            client.get(url.into_unsafe_url()).send().await?.status(),
1567            401,
1568            "Password in the URL should take precedence and fail"
1569        );
1570
1571        let mut url = base_url.clone();
1572        url.set_username(username).unwrap();
1573        assert_eq!(
1574            client
1575                .get(url.clone().into_unsafe_url())
1576                .send()
1577                .await?
1578                .status(),
1579            200,
1580            "Subsequent requests should not use the invalid password"
1581        );
1582
1583        let mut url = base_url.clone();
1584        url.set_username("other_user").unwrap();
1585        assert_eq!(
1586            client.get(url.into_unsafe_url()).send().await?.status(),
1587            401,
1588            "Credentials are not pulled from the keyring when given another username"
1589        );
1590
1591        Ok(())
1592    }
1593
1594    #[test(tokio::test)]
1595    async fn test_keyring_always_authenticate() -> Result<(), Error> {
1596        let username = "user";
1597        let password = "password";
1598        let server = start_test_server(username, password).await;
1599        let base_url = SafeUrl::parse(&server.uri())?;
1600
1601        let indexes = indexes_for(&base_url, AuthPolicy::Always);
1602        let client = test_client_builder()
1603            .with(
1604                AuthMiddleware::new()
1605                    .with_cache(CredentialsCache::new())
1606                    .with_keyring(Some(KeyringProvider::dummy([(
1607                        format!(
1608                            "{}:{}",
1609                            base_url.host_str().unwrap(),
1610                            base_url.port().unwrap()
1611                        ),
1612                        username,
1613                        password,
1614                    )])))
1615                    .with_indexes(indexes),
1616            )
1617            .build();
1618
1619        assert_eq!(
1620            client.get(server.uri()).send().await?.status(),
1621            200,
1622            "Credentials (including a username) should be pulled from the keyring"
1623        );
1624
1625        let mut url = base_url.clone();
1626        url.set_username(username).unwrap();
1627        assert_eq!(
1628            client.get(url.into_unsafe_url()).send().await?.status(),
1629            200,
1630            "The password for the username should be pulled from the keyring"
1631        );
1632
1633        let mut url = base_url.clone();
1634        url.set_username(username).unwrap();
1635        url.set_password(Some("invalid")).unwrap();
1636        assert_eq!(
1637            client.get(url.into_unsafe_url()).send().await?.status(),
1638            401,
1639            "Password in the URL should take precedence and fail"
1640        );
1641
1642        let mut url = base_url.clone();
1643        url.set_username("other_user").unwrap();
1644        assert_matches!(
1645            client.get(url.into_unsafe_url()).send().await,
1646            Err(reqwest_middleware::Error::Middleware(_)),
1647            "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1648        );
1649
1650        Ok(())
1651    }
1652
1653    /// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`,
1654    /// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`.
1655    /// We don't unit test the latter case because it's possible to collide with a server a developer is
1656    /// actually running.
1657    #[test(tokio::test)]
1658    async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1659        let username = "user";
1660        let password = "password";
1661        let server = start_test_server(username, password).await;
1662        let base_url = SafeUrl::parse(&server.uri())?;
1663
1664        let client = test_client_builder()
1665            .with(
1666                AuthMiddleware::new()
1667                    .with_cache(CredentialsCache::new())
1668                    .with_keyring(Some(KeyringProvider::dummy([(
1669                        // Omit the port from the keyring entry
1670                        base_url.host_str().unwrap(),
1671                        username,
1672                        password,
1673                    )]))),
1674            )
1675            .build();
1676
1677        let mut url = base_url.clone();
1678        url.set_username(username).unwrap();
1679        assert_eq!(
1680            client.get(url.into_unsafe_url()).send().await?.status(),
1681            401,
1682            "We should fail because the port is not present in the keyring entry"
1683        );
1684
1685        Ok(())
1686    }
1687
1688    #[test(tokio::test)]
1689    async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1690        let username = "user";
1691        let password = "password";
1692
1693        let server = start_test_server(username, password).await;
1694        let base_url = SafeUrl::parse(&server.uri())?;
1695        let cache = CredentialsCache::new();
1696
1697        // Seed _just_ the username. We should pull the username from the cache if not present on the
1698        // URL.
1699        cache.insert(
1700            &base_url,
1701            Arc::new(Authentication::from(Credentials::basic(
1702                Some(username.to_string()),
1703                None,
1704            ))),
1705        );
1706        let client = test_client_builder()
1707            .with(
1708                AuthMiddleware::new()
1709                    .with_cache(cache)
1710                    .with_keyring(Some(KeyringProvider::dummy([(
1711                        format!(
1712                            "{}:{}",
1713                            base_url.host_str().unwrap(),
1714                            base_url.port().unwrap()
1715                        ),
1716                        username,
1717                        password,
1718                    )])))
1719                    .with_only_authenticated(true)
1720                    .with_credentials_prompt(Some(CredentialsPrompt::new(|| {
1721                        Err(CredentialsPromptError::ReadUsername(io::Error::other(
1722                            "Should not prompt when keyring has credentials",
1723                        )))
1724                    }))),
1725            )
1726            .build();
1727
1728        assert_eq!(
1729            client.get(server.uri()).send().await?.status(),
1730            200,
1731            "The username is pulled from the cache, and the password from the keyring"
1732        );
1733
1734        let mut url = base_url.clone();
1735        url.set_username(username).unwrap();
1736        assert_eq!(
1737            client.get(url.into_unsafe_url()).send().await?.status(),
1738            200,
1739            "Credentials for the username should be pulled from the keyring"
1740        );
1741
1742        Ok(())
1743    }
1744
1745    #[test(tokio::test)]
1746    async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1747        let username_1 = "user1";
1748        let password_1 = "password1";
1749        let server_1 = start_test_server(username_1, password_1).await;
1750        let base_url_1 = SafeUrl::parse(&server_1.uri())?;
1751
1752        let username_2 = "user2";
1753        let password_2 = "password2";
1754        let server_2 = start_test_server(username_2, password_2).await;
1755        let base_url_2 = SafeUrl::parse(&server_2.uri())?;
1756
1757        let cache = CredentialsCache::new();
1758        // Seed the cache with our credentials
1759        cache.insert(
1760            &base_url_1,
1761            Arc::new(Authentication::from(Credentials::basic(
1762                Some(username_1.to_string()),
1763                Some(password_1.to_string()),
1764            ))),
1765        );
1766        cache.insert(
1767            &base_url_2,
1768            Arc::new(Authentication::from(Credentials::basic(
1769                Some(username_2.to_string()),
1770                Some(password_2.to_string()),
1771            ))),
1772        );
1773
1774        let client = test_client_builder()
1775            .with(AuthMiddleware::new().with_cache(cache))
1776            .build();
1777
1778        // Both servers should work
1779        assert_eq!(
1780            client.get(server_1.uri()).send().await?.status(),
1781            200,
1782            "Requests should not require credentials"
1783        );
1784        assert_eq!(
1785            client.get(server_2.uri()).send().await?.status(),
1786            200,
1787            "Requests should not require credentials"
1788        );
1789
1790        assert_eq!(
1791            client
1792                .get(format!("{}/foo", server_1.uri()))
1793                .send()
1794                .await?
1795                .status(),
1796            200,
1797            "Requests can be to different paths in the same realm"
1798        );
1799        assert_eq!(
1800            client
1801                .get(format!("{}/foo", server_2.uri()))
1802                .send()
1803                .await?
1804                .status(),
1805            200,
1806            "Requests can be to different paths in the same realm"
1807        );
1808
1809        Ok(())
1810    }
1811
1812    #[test(tokio::test)]
1813    async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1814        let username_1 = "user1";
1815        let password_1 = "password1";
1816        let server_1 = start_test_server(username_1, password_1).await;
1817        let base_url_1 = SafeUrl::parse(&server_1.uri())?;
1818
1819        let username_2 = "user2";
1820        let password_2 = "password2";
1821        let server_2 = start_test_server(username_2, password_2).await;
1822        let base_url_2 = SafeUrl::parse(&server_2.uri())?;
1823
1824        let client = test_client_builder()
1825            .with(
1826                AuthMiddleware::new()
1827                    .with_cache(CredentialsCache::new())
1828                    .with_keyring(Some(KeyringProvider::dummy([
1829                        (
1830                            format!(
1831                                "{}:{}",
1832                                base_url_1.host_str().unwrap(),
1833                                base_url_1.port().unwrap()
1834                            ),
1835                            username_1,
1836                            password_1,
1837                        ),
1838                        (
1839                            format!(
1840                                "{}:{}",
1841                                base_url_2.host_str().unwrap(),
1842                                base_url_2.port().unwrap()
1843                            ),
1844                            username_2,
1845                            password_2,
1846                        ),
1847                    ]))),
1848            )
1849            .build();
1850
1851        // Both servers do not work without a username
1852        assert_eq!(
1853            client.get(server_1.uri()).send().await?.status(),
1854            401,
1855            "Requests should require a username"
1856        );
1857        assert_eq!(
1858            client.get(server_2.uri()).send().await?.status(),
1859            401,
1860            "Requests should require a username"
1861        );
1862
1863        let mut url_1 = base_url_1.clone();
1864        url_1.set_username(username_1).unwrap();
1865        assert_eq!(
1866            client
1867                .get(url_1.clone().into_unsafe_url())
1868                .send()
1869                .await?
1870                .status(),
1871            200,
1872            "Requests with a username should succeed"
1873        );
1874        assert_eq!(
1875            client.get(server_2.uri()).send().await?.status(),
1876            401,
1877            "Credentials should not be re-used for the second server"
1878        );
1879
1880        let mut url_2 = base_url_2.clone();
1881        url_2.set_username(username_2).unwrap();
1882        assert_eq!(
1883            client
1884                .get(url_2.clone().into_unsafe_url())
1885                .send()
1886                .await?
1887                .status(),
1888            200,
1889            "Requests with a username should succeed"
1890        );
1891
1892        assert_eq!(
1893            client
1894                .get(format!("{}/foo", url_1.as_str()))
1895                .send()
1896                .await?
1897                .status(),
1898            200,
1899            "Requests can be to different paths in the same realm"
1900        );
1901        assert_eq!(
1902            client
1903                .get(format!("{}/foo", url_2.as_str()))
1904                .send()
1905                .await?
1906                .status(),
1907            200,
1908            "Requests can be to different paths in the same realm"
1909        );
1910
1911        Ok(())
1912    }
1913
1914    #[test(tokio::test)]
1915    async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1916        let username_1 = "user1";
1917        let password_1 = "password1";
1918        let username_2 = "user2";
1919        let password_2 = "password2";
1920
1921        let server = MockServer::start().await;
1922
1923        Mock::given(method("GET"))
1924            .and(path_regex("/prefix_1.*"))
1925            .and(basic_auth(username_1, password_1))
1926            .respond_with(ResponseTemplate::new(200))
1927            .mount(&server)
1928            .await;
1929
1930        Mock::given(method("GET"))
1931            .and(path_regex("/prefix_2.*"))
1932            .and(basic_auth(username_2, password_2))
1933            .respond_with(ResponseTemplate::new(200))
1934            .mount(&server)
1935            .await;
1936
1937        // Create a third, public prefix
1938        // It will throw a 401 if it receives credentials
1939        Mock::given(method("GET"))
1940            .and(path_regex("/prefix_3.*"))
1941            .and(basic_auth(username_1, password_1))
1942            .respond_with(ResponseTemplate::new(401))
1943            .mount(&server)
1944            .await;
1945        Mock::given(method("GET"))
1946            .and(path_regex("/prefix_3.*"))
1947            .and(basic_auth(username_2, password_2))
1948            .respond_with(ResponseTemplate::new(401))
1949            .mount(&server)
1950            .await;
1951        Mock::given(method("GET"))
1952            .and(path_regex("/prefix_3.*"))
1953            .respond_with(ResponseTemplate::new(200))
1954            .mount(&server)
1955            .await;
1956
1957        Mock::given(method("GET"))
1958            .respond_with(ResponseTemplate::new(401))
1959            .mount(&server)
1960            .await;
1961
1962        let base_url = SafeUrl::parse(&server.uri())?;
1963        let base_url_1 = base_url.join("prefix_1")?;
1964        let base_url_2 = base_url.join("prefix_2")?;
1965        let base_url_3 = base_url.join("prefix_3")?;
1966
1967        let cache = CredentialsCache::new();
1968
1969        // Seed the cache with our credentials
1970        cache.insert(
1971            &base_url_1,
1972            Arc::new(Authentication::from(Credentials::basic(
1973                Some(username_1.to_string()),
1974                Some(password_1.to_string()),
1975            ))),
1976        );
1977        cache.insert(
1978            &base_url_2,
1979            Arc::new(Authentication::from(Credentials::basic(
1980                Some(username_2.to_string()),
1981                Some(password_2.to_string()),
1982            ))),
1983        );
1984
1985        let client = test_client_builder()
1986            .with(AuthMiddleware::new().with_cache(cache))
1987            .build();
1988
1989        // Both servers should work
1990        assert_eq!(
1991            client
1992                .get(base_url_1.clone().into_unsafe_url())
1993                .send()
1994                .await?
1995                .status(),
1996            200,
1997            "Requests should not require credentials"
1998        );
1999        assert_eq!(
2000            client
2001                .get(base_url_2.clone().into_unsafe_url())
2002                .send()
2003                .await?
2004                .status(),
2005            200,
2006            "Requests should not require credentials"
2007        );
2008        assert_eq!(
2009            client
2010                .get(base_url.join("prefix_1/foo")?.into_unsafe_url())
2011                .send()
2012                .await?
2013                .status(),
2014            200,
2015            "Requests can be to different paths in the same realm"
2016        );
2017        assert_eq!(
2018            client
2019                .get(base_url.join("prefix_2/foo")?.into_unsafe_url())
2020                .send()
2021                .await?
2022                .status(),
2023            200,
2024            "Requests can be to different paths in the same realm"
2025        );
2026        assert_eq!(
2027            client
2028                .get(base_url.join("prefix_1_foo")?.into_unsafe_url())
2029                .send()
2030                .await?
2031                .status(),
2032            401,
2033            "Requests to paths with a matching prefix but different resource segments should fail"
2034        );
2035
2036        assert_eq!(
2037            client
2038                .get(base_url_3.clone().into_unsafe_url())
2039                .send()
2040                .await?
2041                .status(),
2042            200,
2043            "Requests to the 'public' prefix should not use credentials"
2044        );
2045
2046        Ok(())
2047    }
2048
2049    #[test(tokio::test)]
2050    async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
2051        let username_1 = "user1";
2052        let password_1 = "password1";
2053        let username_2 = "user2";
2054        let password_2 = "password2";
2055
2056        let server = MockServer::start().await;
2057
2058        Mock::given(method("GET"))
2059            .and(path_regex("/prefix_1.*"))
2060            .and(basic_auth(username_1, password_1))
2061            .respond_with(ResponseTemplate::new(200))
2062            .mount(&server)
2063            .await;
2064
2065        Mock::given(method("GET"))
2066            .and(path_regex("/prefix_2.*"))
2067            .and(basic_auth(username_2, password_2))
2068            .respond_with(ResponseTemplate::new(200))
2069            .mount(&server)
2070            .await;
2071
2072        // Create a third, public prefix
2073        // It will throw a 401 if it receives credentials
2074        Mock::given(method("GET"))
2075            .and(path_regex("/prefix_3.*"))
2076            .and(basic_auth(username_1, password_1))
2077            .respond_with(ResponseTemplate::new(401))
2078            .mount(&server)
2079            .await;
2080        Mock::given(method("GET"))
2081            .and(path_regex("/prefix_3.*"))
2082            .and(basic_auth(username_2, password_2))
2083            .respond_with(ResponseTemplate::new(401))
2084            .mount(&server)
2085            .await;
2086        Mock::given(method("GET"))
2087            .and(path_regex("/prefix_3.*"))
2088            .respond_with(ResponseTemplate::new(200))
2089            .mount(&server)
2090            .await;
2091
2092        Mock::given(method("GET"))
2093            .respond_with(ResponseTemplate::new(401))
2094            .mount(&server)
2095            .await;
2096
2097        let base_url = SafeUrl::parse(&server.uri())?;
2098        let base_url_1 = base_url.join("prefix_1")?;
2099        let base_url_2 = base_url.join("prefix_2")?;
2100        let base_url_3 = base_url.join("prefix_3")?;
2101
2102        let client = test_client_builder()
2103            .with(
2104                AuthMiddleware::new()
2105                    .with_cache(CredentialsCache::new())
2106                    .with_keyring(Some(KeyringProvider::dummy([
2107                        (
2108                            format!(
2109                                "{}:{}",
2110                                base_url_1.host_str().unwrap(),
2111                                base_url_1.port().unwrap()
2112                            ),
2113                            username_1,
2114                            password_1,
2115                        ),
2116                        (
2117                            format!(
2118                                "{}:{}",
2119                                base_url_2.host_str().unwrap(),
2120                                base_url_2.port().unwrap()
2121                            ),
2122                            username_2,
2123                            password_2,
2124                        ),
2125                    ]))),
2126            )
2127            .build();
2128
2129        // Both servers do not work without a username
2130        assert_eq!(
2131            client
2132                .get(base_url_1.clone().into_unsafe_url())
2133                .send()
2134                .await?
2135                .status(),
2136            401,
2137            "Requests should require a username"
2138        );
2139        assert_eq!(
2140            client
2141                .get(base_url_2.clone().into_unsafe_url())
2142                .send()
2143                .await?
2144                .status(),
2145            401,
2146            "Requests should require a username"
2147        );
2148
2149        let mut url_1 = base_url_1.clone();
2150        url_1.set_username(username_1).unwrap();
2151        assert_eq!(
2152            client
2153                .get(url_1.clone().into_unsafe_url())
2154                .send()
2155                .await?
2156                .status(),
2157            200,
2158            "Requests with a username should succeed"
2159        );
2160        assert_eq!(
2161            client
2162                .get(base_url_2.clone().into_unsafe_url())
2163                .send()
2164                .await?
2165                .status(),
2166            401,
2167            "Credentials should not be re-used for the second prefix"
2168        );
2169
2170        let mut url_2 = base_url_2.clone();
2171        url_2.set_username(username_2).unwrap();
2172        assert_eq!(
2173            client
2174                .get(url_2.clone().into_unsafe_url())
2175                .send()
2176                .await?
2177                .status(),
2178            200,
2179            "Requests with a username should succeed"
2180        );
2181
2182        assert_eq!(
2183            client
2184                .get(base_url.join("prefix_1/foo")?.into_unsafe_url())
2185                .send()
2186                .await?
2187                .status(),
2188            200,
2189            "Requests can be to different paths in the same prefix"
2190        );
2191        assert_eq!(
2192            client
2193                .get(base_url.join("prefix_2/foo")?.into_unsafe_url())
2194                .send()
2195                .await?
2196                .status(),
2197            200,
2198            "Requests can be to different paths in the same prefix"
2199        );
2200        assert_eq!(
2201            client
2202                .get(base_url.join("prefix_1_foo")?.into_unsafe_url())
2203                .send()
2204                .await?
2205                .status(),
2206            401,
2207            "Requests to paths with a matching prefix but different resource segments should fail"
2208        );
2209        assert_eq!(
2210            client
2211                .get(base_url_3.clone().into_unsafe_url())
2212                .send()
2213                .await?
2214                .status(),
2215            200,
2216            "Requests to the 'public' prefix should not use credentials"
2217        );
2218
2219        Ok(())
2220    }
2221
2222    /// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of
2223    /// credentials for _every_ request URL at the cost of inconsistent behavior when
2224    /// credentials are not scoped to a realm.
2225    #[test(tokio::test)]
2226    async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
2227    -> Result<(), Error> {
2228        let username = "user";
2229        let password_1 = "password1";
2230        let password_2 = "password2";
2231
2232        let server = MockServer::start().await;
2233
2234        Mock::given(method("GET"))
2235            .and(path_regex("/prefix_1.*"))
2236            .and(basic_auth(username, password_1))
2237            .respond_with(ResponseTemplate::new(200))
2238            .mount(&server)
2239            .await;
2240
2241        Mock::given(method("GET"))
2242            .and(path_regex("/prefix_2.*"))
2243            .and(basic_auth(username, password_2))
2244            .respond_with(ResponseTemplate::new(200))
2245            .mount(&server)
2246            .await;
2247
2248        Mock::given(method("GET"))
2249            .respond_with(ResponseTemplate::new(401))
2250            .mount(&server)
2251            .await;
2252
2253        let base_url = SafeUrl::parse(&server.uri())?;
2254        let base_url_1 = base_url.join("prefix_1")?;
2255        let base_url_2 = base_url.join("prefix_2")?;
2256
2257        let client = test_client_builder()
2258            .with(
2259                AuthMiddleware::new()
2260                    .with_cache(CredentialsCache::new())
2261                    .with_keyring(Some(KeyringProvider::dummy([
2262                        (base_url_1.as_str(), username, password_1),
2263                        (base_url_2.as_str(), username, password_2),
2264                    ]))),
2265            )
2266            .build();
2267
2268        // Both servers do not work without a username
2269        assert_eq!(
2270            client
2271                .get(base_url_1.clone().into_unsafe_url())
2272                .send()
2273                .await?
2274                .status(),
2275            401,
2276            "Requests should require a username"
2277        );
2278        assert_eq!(
2279            client
2280                .get(base_url_2.clone().into_unsafe_url())
2281                .send()
2282                .await?
2283                .status(),
2284            401,
2285            "Requests should require a username"
2286        );
2287
2288        let mut url_1 = base_url_1.clone();
2289        url_1.set_username(username).unwrap();
2290        assert_eq!(
2291            client
2292                .get(url_1.clone().into_unsafe_url())
2293                .send()
2294                .await?
2295                .status(),
2296            200,
2297            "The first request with a username will succeed"
2298        );
2299        assert_eq!(
2300            client
2301                .get(base_url_2.clone().into_unsafe_url())
2302                .send()
2303                .await?
2304                .status(),
2305            401,
2306            "Credentials should not be re-used for the second prefix"
2307        );
2308        assert_eq!(
2309            client
2310                .get(base_url.join("prefix_1/foo")?.into_unsafe_url())
2311                .send()
2312                .await?
2313                .status(),
2314            200,
2315            "Subsequent requests can be to different paths in the same prefix"
2316        );
2317
2318        let mut url_2 = base_url_2.clone();
2319        url_2.set_username(username).unwrap();
2320        assert_eq!(
2321            client
2322                .get(url_2.clone().into_unsafe_url())
2323                .send()
2324                .await?
2325                .status(),
2326            401, // INCORRECT BEHAVIOR
2327            "A request with the same username and realm for a URL that needs a different password will fail"
2328        );
2329        assert_eq!(
2330            client
2331                .get(base_url.join("prefix_2/foo")?.into_unsafe_url())
2332                .send()
2333                .await?
2334                .status(),
2335            401, // INCORRECT BEHAVIOR
2336            "Requests to other paths in the failing prefix will also fail"
2337        );
2338
2339        Ok(())
2340    }
2341
2342    /// Demonstrates that when an index URL is provided, we avoid "incorrect" behavior
2343    /// where multiple URLs with the same username and realm share the same realm-level
2344    /// credentials cache entry.
2345    #[test(tokio::test)]
2346    async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2347    -> Result<(), Error> {
2348        let username = "user";
2349        let password_1 = "password1";
2350        let password_2 = "password2";
2351
2352        let server = MockServer::start().await;
2353
2354        Mock::given(method("GET"))
2355            .and(path_regex("/prefix_1.*"))
2356            .and(basic_auth(username, password_1))
2357            .respond_with(ResponseTemplate::new(200))
2358            .mount(&server)
2359            .await;
2360
2361        Mock::given(method("GET"))
2362            .and(path_regex("/prefix_2.*"))
2363            .and(basic_auth(username, password_2))
2364            .respond_with(ResponseTemplate::new(200))
2365            .mount(&server)
2366            .await;
2367
2368        Mock::given(method("GET"))
2369            .respond_with(ResponseTemplate::new(401))
2370            .mount(&server)
2371            .await;
2372
2373        let base_url = SafeUrl::parse(&server.uri())?;
2374        let base_url_1 = base_url.join("prefix_1")?;
2375        let base_url_2 = base_url.join("prefix_2")?;
2376        let indexes = Indexes::from_indexes(vec![
2377            Index {
2378                url: base_url_1.clone(),
2379                root_url: base_url_1.clone(),
2380                auth_policy: AuthPolicy::Auto,
2381            },
2382            Index {
2383                url: base_url_2.clone(),
2384                root_url: base_url_2.clone(),
2385                auth_policy: AuthPolicy::Auto,
2386            },
2387        ]);
2388
2389        let client = test_client_builder()
2390            .with(
2391                AuthMiddleware::new()
2392                    .with_cache(CredentialsCache::new())
2393                    .with_keyring(Some(KeyringProvider::dummy([
2394                        (base_url_1.as_str(), username, password_1),
2395                        (base_url_2.as_str(), username, password_2),
2396                    ])))
2397                    .with_indexes(indexes),
2398            )
2399            .build();
2400
2401        // Both servers do not work without a username
2402        assert_eq!(
2403            client
2404                .get(base_url_1.clone().into_unsafe_url())
2405                .send()
2406                .await?
2407                .status(),
2408            401,
2409            "Requests should require a username"
2410        );
2411        assert_eq!(
2412            client
2413                .get(base_url_2.clone().into_unsafe_url())
2414                .send()
2415                .await?
2416                .status(),
2417            401,
2418            "Requests should require a username"
2419        );
2420
2421        let mut url_1 = base_url_1.clone();
2422        url_1.set_username(username).unwrap();
2423        assert_eq!(
2424            client
2425                .get(url_1.clone().into_unsafe_url())
2426                .send()
2427                .await?
2428                .status(),
2429            200,
2430            "The first request with a username will succeed"
2431        );
2432        assert_eq!(
2433            client
2434                .get(base_url_2.clone().into_unsafe_url())
2435                .send()
2436                .await?
2437                .status(),
2438            401,
2439            "Credentials should not be re-used for the second prefix"
2440        );
2441        assert_eq!(
2442            client
2443                .get(base_url.join("prefix_1/foo")?.into_unsafe_url())
2444                .send()
2445                .await?
2446                .status(),
2447            200,
2448            "Subsequent requests can be to different paths in the same prefix"
2449        );
2450
2451        let mut url_2 = base_url_2.clone();
2452        url_2.set_username(username).unwrap();
2453        assert_eq!(
2454            client
2455                .get(url_2.clone().into_unsafe_url())
2456                .send()
2457                .await?
2458                .status(),
2459            200,
2460            "A request with the same username and realm for a URL will use index-specific password"
2461        );
2462        assert_eq!(
2463            client
2464                .get(base_url.join("prefix_2/foo")?.into_unsafe_url())
2465                .send()
2466                .await?
2467                .status(),
2468            200,
2469            "Requests to other paths with that prefix will also succeed"
2470        );
2471
2472        Ok(())
2473    }
2474
2475    /// Demonstrates that when an index' credentials are cached for its realm, we
2476    /// find those credentials if they're not present in the keyring.
2477    #[test(tokio::test)]
2478    async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2479    -> Result<(), Error> {
2480        let username = "user";
2481        let password = "password";
2482
2483        let server = MockServer::start().await;
2484
2485        Mock::given(method("GET"))
2486            .and(basic_auth(username, password))
2487            .respond_with(ResponseTemplate::new(200))
2488            .mount(&server)
2489            .await;
2490
2491        Mock::given(method("GET"))
2492            .and(path_regex("/prefix_1.*"))
2493            .and(basic_auth(username, password))
2494            .respond_with(ResponseTemplate::new(200))
2495            .mount(&server)
2496            .await;
2497
2498        Mock::given(method("GET"))
2499            .respond_with(ResponseTemplate::new(401))
2500            .mount(&server)
2501            .await;
2502
2503        let base_url = SafeUrl::parse(&server.uri())?;
2504        let index_url = base_url.join("prefix_1")?;
2505        let indexes = Indexes::from_indexes(vec![Index {
2506            url: index_url.clone(),
2507            root_url: index_url.clone(),
2508            auth_policy: AuthPolicy::Auto,
2509        }]);
2510
2511        let client = test_client_builder()
2512            .with(
2513                AuthMiddleware::new()
2514                    .with_cache(CredentialsCache::new())
2515                    .with_keyring(Some(KeyringProvider::dummy([(
2516                        base_url.as_str(),
2517                        username,
2518                        password,
2519                    )])))
2520                    .with_indexes(indexes),
2521            )
2522            .build();
2523
2524        // Index server does not work without a username
2525        assert_eq!(
2526            client
2527                .get(index_url.clone().into_unsafe_url())
2528                .send()
2529                .await?
2530                .status(),
2531            401,
2532            "Requests should require a username"
2533        );
2534
2535        // Send a request that will cache realm credentials.
2536        let mut realm_url = base_url.clone();
2537        realm_url.set_username(username).unwrap();
2538        assert_eq!(
2539            client
2540                .get(realm_url.clone().into_unsafe_url())
2541                .send()
2542                .await?
2543                .status(),
2544            200,
2545            "The first realm request with a username will succeed"
2546        );
2547
2548        let mut url = index_url.clone();
2549        url.set_username(username).unwrap();
2550        assert_eq!(
2551            client
2552                .get(url.clone().into_unsafe_url())
2553                .send()
2554                .await?
2555                .status(),
2556            200,
2557            "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2558        );
2559        assert_eq!(
2560            client
2561                .get(base_url.join("prefix_1/foo")?.into_unsafe_url())
2562                .send()
2563                .await?
2564                .status(),
2565            200,
2566            "Requests to other paths with that prefix will also succeed"
2567        );
2568
2569        Ok(())
2570    }
2571
2572    fn indexes_for(url: &SafeUrl, policy: AuthPolicy) -> Indexes {
2573        let mut url = url.clone();
2574        url.set_password(None).ok();
2575        url.set_username("").ok();
2576        Indexes::from_indexes(vec![Index {
2577            url: url.clone(),
2578            root_url: url.clone(),
2579            auth_policy: policy,
2580        }])
2581    }
2582
2583    /// With the "always" auth policy, requests should succeed on
2584    /// authenticated requests with the correct credentials.
2585    #[test(tokio::test)]
2586    async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2587        let username = "user";
2588        let password = "password";
2589
2590        let server = start_test_server(username, password).await;
2591
2592        let base_url = SafeUrl::parse(&server.uri())?;
2593
2594        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2595        let client = test_client_builder()
2596            .with(
2597                AuthMiddleware::new()
2598                    .with_cache(CredentialsCache::new())
2599                    .with_indexes(indexes),
2600            )
2601            .build();
2602
2603        Mock::given(method("GET"))
2604            .and(path_regex("/*"))
2605            .and(basic_auth(username, password))
2606            .respond_with(ResponseTemplate::new(200))
2607            .mount(&server)
2608            .await;
2609
2610        Mock::given(method("GET"))
2611            .respond_with(ResponseTemplate::new(401))
2612            .mount(&server)
2613            .await;
2614
2615        let mut url = base_url.clone();
2616        url.set_username(username).unwrap();
2617        url.set_password(Some(password)).unwrap();
2618        assert_eq!(
2619            client.get(url.into_unsafe_url()).send().await?.status(),
2620            200
2621        );
2622
2623        assert_eq!(
2624            client
2625                .get(format!("{}/foo", server.uri()))
2626                .send()
2627                .await?
2628                .status(),
2629            200,
2630            "Requests can be to different paths with index URL as prefix"
2631        );
2632
2633        let mut url = base_url.clone();
2634        url.set_username(username).unwrap();
2635        url.set_password(Some("invalid")).unwrap();
2636        assert_eq!(
2637            client.get(url.into_unsafe_url()).send().await?.status(),
2638            401,
2639            "Incorrect credentials should fail"
2640        );
2641
2642        Ok(())
2643    }
2644
2645    /// With the "always" auth policy, requests should fail if only
2646    /// unauthenticated requests are supported.
2647    #[test(tokio::test)]
2648    async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2649        let server = MockServer::start().await;
2650
2651        Mock::given(method("GET"))
2652            .and(path_regex("/*"))
2653            .respond_with(ResponseTemplate::new(200))
2654            .mount(&server)
2655            .await;
2656
2657        Mock::given(method("GET"))
2658            .respond_with(ResponseTemplate::new(401))
2659            .mount(&server)
2660            .await;
2661
2662        let base_url = SafeUrl::parse(&server.uri())?;
2663
2664        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2665        let client = test_client_builder()
2666            .with(
2667                AuthMiddleware::new()
2668                    .with_cache(CredentialsCache::new())
2669                    .with_indexes(indexes),
2670            )
2671            .build();
2672
2673        // Unauthenticated requests are not allowed.
2674        assert_matches!(
2675            client.get(server.uri()).send().await,
2676            Err(reqwest_middleware::Error::Middleware(_))
2677        );
2678
2679        Ok(())
2680    }
2681
2682    /// With the "never" auth policy, requests should fail if
2683    /// an endpoint requires authentication.
2684    #[test(tokio::test)]
2685    async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2686        let username = "user";
2687        let password = "password";
2688
2689        let server = start_test_server(username, password).await;
2690        let base_url = SafeUrl::parse(&server.uri())?;
2691
2692        Mock::given(method("GET"))
2693            .and(path_regex("/*"))
2694            .and(basic_auth(username, password))
2695            .respond_with(ResponseTemplate::new(200))
2696            .mount(&server)
2697            .await;
2698
2699        Mock::given(method("GET"))
2700            .respond_with(ResponseTemplate::new(401))
2701            .mount(&server)
2702            .await;
2703
2704        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2705        let client = test_client_builder()
2706            .with(
2707                AuthMiddleware::new()
2708                    .with_cache(CredentialsCache::new())
2709                    .with_indexes(indexes),
2710            )
2711            .build();
2712
2713        let mut url = base_url.clone();
2714        url.set_username(username).unwrap();
2715        url.set_password(Some(password)).unwrap();
2716
2717        assert_eq!(
2718            client
2719                .get(format!("{}/foo", server.uri()))
2720                .send()
2721                .await?
2722                .status(),
2723            401,
2724            "Requests should not be completed if credentials are required"
2725        );
2726
2727        Ok(())
2728    }
2729
2730    /// With the "never" auth policy, requests should succeed if
2731    /// unauthenticated requests succeed.
2732    #[test(tokio::test)]
2733    async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2734        let server = MockServer::start().await;
2735
2736        Mock::given(method("GET"))
2737            .and(path_regex("/*"))
2738            .respond_with(ResponseTemplate::new(200))
2739            .mount(&server)
2740            .await;
2741
2742        Mock::given(method("GET"))
2743            .respond_with(ResponseTemplate::new(401))
2744            .mount(&server)
2745            .await;
2746
2747        let base_url = SafeUrl::parse(&server.uri())?;
2748
2749        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2750        let client = test_client_builder()
2751            .with(
2752                AuthMiddleware::new()
2753                    .with_cache(CredentialsCache::new())
2754                    .with_indexes(indexes),
2755            )
2756            .build();
2757
2758        assert_eq!(
2759            client.get(server.uri()).send().await?.status(),
2760            200,
2761            "Requests should succeed if unauthenticated requests can succeed"
2762        );
2763
2764        Ok(())
2765    }
2766
2767    #[test]
2768    fn test_tracing_url() {
2769        // No credentials
2770        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2771        assert_eq!(
2772            tracing_url(&req, None),
2773            SafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2774        );
2775
2776        let creds = Authentication::from(Credentials::Basic {
2777            username: Username::new(Some(String::from("user"))),
2778            password: None,
2779        });
2780        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2781        assert_eq!(
2782            tracing_url(&req, Some(&creds)),
2783            SafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2784        );
2785
2786        let creds = Authentication::from(Credentials::Basic {
2787            username: Username::new(Some(String::from("user"))),
2788            password: Some(Password::new(String::from("password"))),
2789        });
2790        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2791        assert_eq!(
2792            tracing_url(&req, Some(&creds)),
2793            SafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2794        );
2795    }
2796
2797    #[test(tokio::test)]
2798    async fn test_text_store_basic_auth() -> Result<(), Error> {
2799        let username = "user";
2800        let password = "password";
2801
2802        let server = start_test_server(username, password).await;
2803        let base_url = SafeUrl::parse(&server.uri())?;
2804
2805        // Create a text credential store with matching credentials
2806        let mut store = TextCredentialStore::default();
2807        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2808        let credentials =
2809            Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2810        store.insert(service.clone(), credentials);
2811
2812        let client = test_client_builder()
2813            .with(
2814                AuthMiddleware::new()
2815                    .with_cache(CredentialsCache::new())
2816                    .with_text_store(Some(store)),
2817            )
2818            .build();
2819
2820        assert_eq!(
2821            client.get(server.uri()).send().await?.status(),
2822            200,
2823            "Credentials should be pulled from the text store"
2824        );
2825
2826        Ok(())
2827    }
2828
2829    #[test(tokio::test)]
2830    async fn test_text_store_disabled() -> Result<(), Error> {
2831        let username = "user";
2832        let password = "password";
2833        let server = start_test_server(username, password).await;
2834
2835        let client = test_client_builder()
2836            .with(
2837                AuthMiddleware::new()
2838                    .with_cache(CredentialsCache::new())
2839                    .with_text_store(None), // Explicitly disable text store
2840            )
2841            .build();
2842
2843        assert_eq!(
2844            client.get(server.uri()).send().await?.status(),
2845            401,
2846            "Credentials should not be found when text store is disabled"
2847        );
2848
2849        Ok(())
2850    }
2851
2852    #[test(tokio::test)]
2853    async fn test_text_store_by_username() -> Result<(), Error> {
2854        let username = "testuser";
2855        let password = "testpass";
2856        let wrong_username = "wronguser";
2857
2858        let server = start_test_server(username, password).await;
2859        let base_url = SafeUrl::parse(&server.uri())?;
2860
2861        let mut store = TextCredentialStore::default();
2862        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2863        let credentials =
2864            crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2865        store.insert(service.clone(), credentials);
2866
2867        let client = test_client_builder()
2868            .with(
2869                AuthMiddleware::new()
2870                    .with_cache(CredentialsCache::new())
2871                    .with_text_store(Some(store)),
2872            )
2873            .build();
2874
2875        // Request with matching username should succeed
2876        let url_with_username = format!(
2877            "{}://{}@{}",
2878            base_url.scheme(),
2879            username,
2880            base_url.host_str().unwrap()
2881        );
2882        let url_with_port = if let Some(port) = base_url.port() {
2883            format!("{}:{}{}", url_with_username, port, base_url.path())
2884        } else {
2885            format!("{}{}", url_with_username, base_url.path())
2886        };
2887
2888        assert_eq!(
2889            client.get(&url_with_port).send().await?.status(),
2890            200,
2891            "Request with matching username should succeed"
2892        );
2893
2894        // Request with non-matching username should fail
2895        let url_with_wrong_username = format!(
2896            "{}://{}@{}",
2897            base_url.scheme(),
2898            wrong_username,
2899            base_url.host_str().unwrap()
2900        );
2901        let url_with_port = if let Some(port) = base_url.port() {
2902            format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2903        } else {
2904            format!("{}{}", url_with_wrong_username, base_url.path())
2905        };
2906
2907        assert_eq!(
2908            client.get(&url_with_port).send().await?.status(),
2909            401,
2910            "Request with non-matching username should fail"
2911        );
2912
2913        // Request without username should succeed
2914        assert_eq!(
2915            client.get(server.uri()).send().await?.status(),
2916            200,
2917            "Request with no username should succeed"
2918        );
2919
2920        Ok(())
2921    }
2922
2923    fn create_request(url: &str) -> Request {
2924        Request::new(Method::GET, SafeUrl::parse(url).unwrap().into_unsafe_url())
2925    }
2926
2927    /// Test for <https://github.com/astral-sh/uv/issues/17343>
2928    ///
2929    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
2930    /// should be recognized as having credentials and authenticate successfully.
2931    #[test(tokio::test)]
2932    async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2933        let username = "";
2934        let password = "token";
2935
2936        let server = MockServer::start().await;
2937
2938        Mock::given(method("GET"))
2939            .and(basic_auth(username, password))
2940            .respond_with(ResponseTemplate::new(200))
2941            .mount(&server)
2942            .await;
2943
2944        Mock::given(method("GET"))
2945            .respond_with(ResponseTemplate::new(401))
2946            .mount(&server)
2947            .await;
2948
2949        let client = test_client_builder()
2950            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2951            .build();
2952
2953        let base_url = SafeUrl::parse(&server.uri())?;
2954
2955        // Test with the URL format `:password@host` (empty username, password present)
2956        let mut url = base_url.clone();
2957        url.set_password(Some(password)).unwrap();
2958        assert_eq!(
2959            client.get(url.into_unsafe_url()).send().await?.status(),
2960            200,
2961            "URL with empty username but password should authenticate successfully"
2962        );
2963
2964        // Subsequent requests to the same realm should also succeed (credentials cached)
2965        assert_eq!(
2966            client.get(server.uri()).send().await?.status(),
2967            200,
2968            "Subsequent requests should use cached credentials"
2969        );
2970
2971        assert_eq!(
2972            client
2973                .get(format!("{}/foo", server.uri()))
2974                .send()
2975                .await?
2976                .status(),
2977            200,
2978            "Requests to different paths in the same realm should succeed"
2979        );
2980
2981        Ok(())
2982    }
2983}