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