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, or 404 check for authentication if there was a cache miss
330    ///     - Check the cache (index URL or realm key) for the username and password
331    ///     - Check the netrc for a username and password
332    ///     - Perform the request again if found
333    ///     - Add the username and password to the cache if successful
334    async fn handle(
335        &self,
336        mut request: Request,
337        extensions: &mut Extensions,
338        next: Next<'_>,
339    ) -> reqwest_middleware::Result<Response> {
340        // Check for credentials attached to the request already
341        let request_credentials = Credentials::from_request(&request)?.map(Authentication::from);
342
343        // In the middleware, existing credentials are already moved from the URL
344        // to the headers so for display purposes we restore some information
345        let url = tracing_url(&request, request_credentials.as_ref());
346        let index = self.indexes.index_for(request.url());
347        let auth_policy = self.indexes.auth_policy_for(request.url());
348        trace!("Handling request for {url} with authentication policy {auth_policy}");
349
350        let credentials: Option<Arc<Authentication>> = if matches!(auth_policy, AuthPolicy::Never) {
351            None
352        } else {
353            if let Some(request_credentials) = request_credentials {
354                return self
355                    .complete_request_with_request_credentials(
356                        request_credentials,
357                        request,
358                        extensions,
359                        next,
360                        &url,
361                        index,
362                        auth_policy,
363                    )
364                    .await;
365            }
366
367            // We have no credentials
368            trace!("Request for {url} is unauthenticated, checking cache");
369
370            // Check the cache for a URL match first. This can save us from
371            // making a failing request
372            let credentials = self
373                .cache()
374                .get_url(DisplaySafeUrl::ref_cast(request.url()), &Username::none());
375            if let Some(credentials) = credentials.as_ref() {
376                request = credentials.authenticate(request).await?;
377
378                // If it's fully authenticated, finish the request
379                if credentials.is_authenticated() {
380                    trace!("Request for {url} is fully authenticated");
381                    return self
382                        .complete_request(None, request, extensions, next, auth_policy)
383                        .await;
384                }
385
386                // If we just found a username, we'll make the request then look for password elsewhere
387                // if it fails
388                trace!("Found username for {url} in cache, attempting request");
389            }
390            credentials
391        };
392        let attempt_has_username = credentials
393            .as_ref()
394            .is_some_and(|credentials| credentials.username().is_some());
395
396        let must_authenticate = self.only_authenticated
397            || (matches!(auth_policy, AuthPolicy::Always)
398                // Dependabot intercepts HTTP requests and injects credentials, which means that we
399                // cannot eagerly enforce an `AuthPolicy` as we don't know whether credentials will be
400                // added outside of uv.
401                && !*IS_DEPENDABOT);
402
403        let (mut retry_request, response) = if !must_authenticate {
404            let url = tracing_url(&request, credentials.as_deref());
405            if credentials.is_none() {
406                trace!("Attempting unauthenticated request for {url}");
407            } else {
408                trace!("Attempting partially authenticated request for {url}");
409            }
410
411            // <https://github.com/TrueLayer/reqwest-middleware/blob/abdf1844c37092d323683c2396b7eefda1418d3c/reqwest-retry/src/middleware.rs#L141-L149>
412            // Clone the request so we can retry it on authentication failure
413            let retry_request = request.try_clone().ok_or_else(|| {
414                Error::Middleware(anyhow!(
415                    "Request object is not cloneable. Are you passing a streaming body?"
416                        .to_string()
417                ))
418            })?;
419
420            let response = next.clone().run(request, extensions).await?;
421
422            // If we don't fail with authorization related codes or
423            // authentication policy is Never, return the response.
424            if !matches!(
425                response.status(),
426                StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED
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
879fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl {
880    let mut url = DisplaySafeUrl::from_url(request.url().clone());
881    if let Some(Authentication::Credentials(creds)) = credentials {
882        if let Some(username) = creds.username() {
883            let _ = url.set_username(username);
884        }
885        if let Some(password) = creds.password() {
886            let _ = url.set_password(Some(password));
887        }
888    }
889    url
890}
891
892#[cfg(test)]
893mod tests {
894    use std::assert_matches;
895    use std::io::Write;
896
897    use http::Method;
898    use reqwest::Client;
899    use tempfile::NamedTempFile;
900    use test_log::test;
901
902    use url::Url;
903    use wiremock::matchers::{basic_auth, method, path_regex};
904    use wiremock::{Mock, MockServer, ResponseTemplate};
905
906    use crate::Index;
907    use crate::credentials::Password;
908
909    use super::*;
910
911    type Error = Box<dyn std::error::Error>;
912
913    async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
914        let server = MockServer::start().await;
915
916        Mock::given(method("GET"))
917            .and(basic_auth(username, password))
918            .respond_with(ResponseTemplate::new(200))
919            .mount(&server)
920            .await;
921
922        Mock::given(method("GET"))
923            .respond_with(ResponseTemplate::new(401))
924            .mount(&server)
925            .await;
926
927        server
928    }
929
930    fn test_client_builder() -> reqwest_middleware::ClientBuilder {
931        reqwest_middleware::ClientBuilder::new(
932            Client::builder()
933                .build()
934                .expect("Reqwest client should build"),
935        )
936    }
937
938    #[test(tokio::test)]
939    async fn test_no_credentials() -> Result<(), Error> {
940        let server = start_test_server("user", "password").await;
941        let client = test_client_builder()
942            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
943            .build();
944
945        assert_eq!(
946            client
947                .get(format!("{}/foo", server.uri()))
948                .send()
949                .await?
950                .status(),
951            401
952        );
953
954        assert_eq!(
955            client
956                .get(format!("{}/bar", server.uri()))
957                .send()
958                .await?
959                .status(),
960            401
961        );
962
963        Ok(())
964    }
965
966    /// Without seeding the cache, authenticated requests are not cached
967    #[test(tokio::test)]
968    async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
969        let username = "user";
970        let password = "password";
971
972        let server = start_test_server(username, password).await;
973        let client = test_client_builder()
974            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
975            .build();
976
977        let base_url = Url::parse(&server.uri())?;
978
979        let mut url = base_url.clone();
980        url.set_username(username).unwrap();
981        url.set_password(Some(password)).unwrap();
982        assert_eq!(client.get(url).send().await?.status(), 200);
983
984        // Works for a URL without credentials now
985        assert_eq!(
986            client.get(server.uri()).send().await?.status(),
987            200,
988            "Subsequent requests should not require credentials"
989        );
990
991        assert_eq!(
992            client
993                .get(format!("{}/foo", server.uri()))
994                .send()
995                .await?
996                .status(),
997            200,
998            "Requests can be to different paths in the same realm"
999        );
1000
1001        let mut url = base_url.clone();
1002        url.set_username(username).unwrap();
1003        url.set_password(Some("invalid")).unwrap();
1004        assert_eq!(
1005            client.get(url).send().await?.status(),
1006            401,
1007            "Credentials in the URL should take precedence and fail"
1008        );
1009
1010        Ok(())
1011    }
1012
1013    #[test(tokio::test)]
1014    async fn test_credentials_in_url_seed() -> Result<(), Error> {
1015        let username = "user";
1016        let password = "password";
1017
1018        let server = start_test_server(username, password).await;
1019        let base_url = Url::parse(&server.uri())?;
1020        let cache = CredentialsCache::new();
1021        cache.insert(
1022            DisplaySafeUrl::ref_cast(&base_url),
1023            Arc::new(Authentication::from(Credentials::basic(
1024                Some(username.to_string()),
1025                Some(password.to_string()),
1026            ))),
1027        );
1028
1029        let client = test_client_builder()
1030            .with(AuthMiddleware::new().with_cache(cache))
1031            .build();
1032
1033        let mut url = base_url.clone();
1034        url.set_username(username).unwrap();
1035        url.set_password(Some(password)).unwrap();
1036        assert_eq!(client.get(url).send().await?.status(), 200);
1037
1038        // Works for a URL without credentials too
1039        assert_eq!(
1040            client.get(server.uri()).send().await?.status(),
1041            200,
1042            "Requests should not require credentials"
1043        );
1044
1045        assert_eq!(
1046            client
1047                .get(format!("{}/foo", server.uri()))
1048                .send()
1049                .await?
1050                .status(),
1051            200,
1052            "Requests can be to different paths in the same realm"
1053        );
1054
1055        let mut url = base_url.clone();
1056        url.set_username(username).unwrap();
1057        url.set_password(Some("invalid")).unwrap();
1058        assert_eq!(
1059            client.get(url).send().await?.status(),
1060            401,
1061            "Credentials in the URL should take precedence and fail"
1062        );
1063
1064        Ok(())
1065    }
1066
1067    #[test(tokio::test)]
1068    async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1069        let username = "user";
1070        let password = "";
1071
1072        let server = start_test_server(username, password).await;
1073        let base_url = Url::parse(&server.uri())?;
1074        let cache = CredentialsCache::new();
1075        cache.insert(
1076            DisplaySafeUrl::ref_cast(&base_url),
1077            Arc::new(Authentication::from(Credentials::basic(
1078                Some(username.to_string()),
1079                None,
1080            ))),
1081        );
1082
1083        let client = test_client_builder()
1084            .with(AuthMiddleware::new().with_cache(cache))
1085            .build();
1086
1087        let mut url = base_url.clone();
1088        url.set_username(username).unwrap();
1089        url.set_password(None).unwrap();
1090        assert_eq!(client.get(url).send().await?.status(), 200);
1091
1092        // Works for a URL without credentials too
1093        assert_eq!(
1094            client.get(server.uri()).send().await?.status(),
1095            200,
1096            "Requests should not require credentials"
1097        );
1098
1099        assert_eq!(
1100            client
1101                .get(format!("{}/foo", server.uri()))
1102                .send()
1103                .await?
1104                .status(),
1105            200,
1106            "Requests can be to different paths in the same realm"
1107        );
1108
1109        let mut url = base_url.clone();
1110        url.set_username(username).unwrap();
1111        url.set_password(Some("invalid")).unwrap();
1112        assert_eq!(
1113            client.get(url).send().await?.status(),
1114            401,
1115            "Credentials in the URL should take precedence and fail"
1116        );
1117
1118        assert_eq!(
1119            client.get(server.uri()).send().await?.status(),
1120            200,
1121            "Subsequent requests should not use the invalid credentials"
1122        );
1123
1124        Ok(())
1125    }
1126
1127    #[test(tokio::test)]
1128    async fn test_netrc_file_default_host() -> Result<(), Error> {
1129        let username = "user";
1130        let password = "password";
1131
1132        let mut netrc_file = NamedTempFile::new()?;
1133        writeln!(netrc_file, "default login {username} password {password}")?;
1134
1135        let server = start_test_server(username, password).await;
1136        let client = test_client_builder()
1137            .with(
1138                AuthMiddleware::new()
1139                    .with_cache(CredentialsCache::new())
1140                    .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1141            )
1142            .build();
1143
1144        assert_eq!(
1145            client.get(server.uri()).send().await?.status(),
1146            200,
1147            "Credentials should be pulled from the netrc file"
1148        );
1149
1150        let mut url = Url::parse(&server.uri())?;
1151        url.set_username(username).unwrap();
1152        url.set_password(Some("invalid")).unwrap();
1153        assert_eq!(
1154            client.get(url).send().await?.status(),
1155            401,
1156            "Credentials in the URL should take precedence and fail"
1157        );
1158
1159        assert_eq!(
1160            client.get(server.uri()).send().await?.status(),
1161            200,
1162            "Subsequent requests should not use the invalid credentials"
1163        );
1164
1165        Ok(())
1166    }
1167
1168    #[test(tokio::test)]
1169    async fn test_netrc_file_matching_host() -> Result<(), Error> {
1170        let username = "user";
1171        let password = "password";
1172        let server = start_test_server(username, password).await;
1173        let base_url = Url::parse(&server.uri())?;
1174
1175        let mut netrc_file = NamedTempFile::new()?;
1176        writeln!(
1177            netrc_file,
1178            r"machine {} login {username} password {password}",
1179            base_url.host_str().unwrap()
1180        )?;
1181
1182        let client = test_client_builder()
1183            .with(
1184                AuthMiddleware::new()
1185                    .with_cache(CredentialsCache::new())
1186                    .with_netrc(Some(
1187                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1188                    )),
1189            )
1190            .build();
1191
1192        assert_eq!(
1193            client.get(server.uri()).send().await?.status(),
1194            200,
1195            "Credentials should be pulled from the netrc file"
1196        );
1197
1198        let mut url = base_url.clone();
1199        url.set_username(username).unwrap();
1200        url.set_password(Some("invalid")).unwrap();
1201        assert_eq!(
1202            client.get(url).send().await?.status(),
1203            401,
1204            "Credentials in the URL should take precedence and fail"
1205        );
1206
1207        assert_eq!(
1208            client.get(server.uri()).send().await?.status(),
1209            200,
1210            "Subsequent requests should not use the invalid credentials"
1211        );
1212
1213        Ok(())
1214    }
1215
1216    #[test(tokio::test)]
1217    async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1218        let username = "user";
1219        let password = "password";
1220        let server = start_test_server(username, password).await;
1221
1222        let mut netrc_file = NamedTempFile::new()?;
1223        writeln!(
1224            netrc_file,
1225            r"machine example.com login {username} password {password}",
1226        )?;
1227
1228        let client = test_client_builder()
1229            .with(
1230                AuthMiddleware::new()
1231                    .with_cache(CredentialsCache::new())
1232                    .with_netrc(Some(
1233                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1234                    )),
1235            )
1236            .build();
1237
1238        assert_eq!(
1239            client.get(server.uri()).send().await?.status(),
1240            401,
1241            "Credentials should not be pulled from the netrc file due to host mismatch"
1242        );
1243
1244        let mut url = Url::parse(&server.uri())?;
1245        url.set_username(username).unwrap();
1246        url.set_password(Some(password)).unwrap();
1247        assert_eq!(
1248            client.get(url).send().await?.status(),
1249            200,
1250            "Credentials in the URL should still work"
1251        );
1252
1253        Ok(())
1254    }
1255
1256    #[test(tokio::test)]
1257    async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1258        let username = "user";
1259        let password = "password";
1260        let server = start_test_server(username, password).await;
1261        let base_url = Url::parse(&server.uri())?;
1262
1263        let mut netrc_file = NamedTempFile::new()?;
1264        writeln!(
1265            netrc_file,
1266            r"machine {} login {username} password {password}",
1267            base_url.host_str().unwrap()
1268        )?;
1269
1270        let client = test_client_builder()
1271            .with(
1272                AuthMiddleware::new()
1273                    .with_cache(CredentialsCache::new())
1274                    .with_netrc(Some(
1275                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1276                    )),
1277            )
1278            .build();
1279
1280        let mut url = base_url.clone();
1281        url.set_username("other-user").unwrap();
1282        assert_eq!(
1283            client.get(url).send().await?.status(),
1284            401,
1285            "The netrc password should not be used due to a username mismatch"
1286        );
1287
1288        let mut url = base_url.clone();
1289        url.set_username("user").unwrap();
1290        assert_eq!(
1291            client.get(url).send().await?.status(),
1292            200,
1293            "The netrc password should be used for a matching user"
1294        );
1295
1296        Ok(())
1297    }
1298
1299    #[test(tokio::test)]
1300    async fn test_keyring() -> Result<(), Error> {
1301        let username = "user";
1302        let password = "password";
1303        let server = start_test_server(username, password).await;
1304        let base_url = Url::parse(&server.uri())?;
1305
1306        let client = test_client_builder()
1307            .with(
1308                AuthMiddleware::new()
1309                    .with_cache(CredentialsCache::new())
1310                    .with_keyring(Some(KeyringProvider::dummy([(
1311                        format!(
1312                            "{}:{}",
1313                            base_url.host_str().unwrap(),
1314                            base_url.port().unwrap()
1315                        ),
1316                        username,
1317                        password,
1318                    )]))),
1319            )
1320            .build();
1321
1322        assert_eq!(
1323            client.get(server.uri()).send().await?.status(),
1324            401,
1325            "Credentials are not pulled from the keyring without a username"
1326        );
1327
1328        let mut url = base_url.clone();
1329        url.set_username(username).unwrap();
1330        assert_eq!(
1331            client.get(url).send().await?.status(),
1332            200,
1333            "Credentials for the username should be pulled from the keyring"
1334        );
1335
1336        let mut url = base_url.clone();
1337        url.set_username(username).unwrap();
1338        url.set_password(Some("invalid")).unwrap();
1339        assert_eq!(
1340            client.get(url).send().await?.status(),
1341            401,
1342            "Password in the URL should take precedence and fail"
1343        );
1344
1345        let mut url = base_url.clone();
1346        url.set_username(username).unwrap();
1347        assert_eq!(
1348            client.get(url.clone()).send().await?.status(),
1349            200,
1350            "Subsequent requests should not use the invalid password"
1351        );
1352
1353        let mut url = base_url.clone();
1354        url.set_username("other_user").unwrap();
1355        assert_eq!(
1356            client.get(url).send().await?.status(),
1357            401,
1358            "Credentials are not pulled from the keyring when given another username"
1359        );
1360
1361        Ok(())
1362    }
1363
1364    #[test(tokio::test)]
1365    async fn test_keyring_always_authenticate() -> Result<(), Error> {
1366        let username = "user";
1367        let password = "password";
1368        let server = start_test_server(username, password).await;
1369        let base_url = Url::parse(&server.uri())?;
1370
1371        let indexes = indexes_for(&base_url, AuthPolicy::Always);
1372        let client = test_client_builder()
1373            .with(
1374                AuthMiddleware::new()
1375                    .with_cache(CredentialsCache::new())
1376                    .with_keyring(Some(KeyringProvider::dummy([(
1377                        format!(
1378                            "{}:{}",
1379                            base_url.host_str().unwrap(),
1380                            base_url.port().unwrap()
1381                        ),
1382                        username,
1383                        password,
1384                    )])))
1385                    .with_indexes(indexes),
1386            )
1387            .build();
1388
1389        assert_eq!(
1390            client.get(server.uri()).send().await?.status(),
1391            200,
1392            "Credentials (including a username) should be pulled from the keyring"
1393        );
1394
1395        let mut url = base_url.clone();
1396        url.set_username(username).unwrap();
1397        assert_eq!(
1398            client.get(url).send().await?.status(),
1399            200,
1400            "The password for the username should be pulled from the keyring"
1401        );
1402
1403        let mut url = base_url.clone();
1404        url.set_username(username).unwrap();
1405        url.set_password(Some("invalid")).unwrap();
1406        assert_eq!(
1407            client.get(url).send().await?.status(),
1408            401,
1409            "Password in the URL should take precedence and fail"
1410        );
1411
1412        let mut url = base_url.clone();
1413        url.set_username("other_user").unwrap();
1414        assert_matches!(
1415            client.get(url).send().await,
1416            Err(reqwest_middleware::Error::Middleware(_)),
1417            "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1418        );
1419
1420        Ok(())
1421    }
1422
1423    /// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`,
1424    /// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`.
1425    /// We don't unit test the latter case because it's possible to collide with a server a developer is
1426    /// actually running.
1427    #[test(tokio::test)]
1428    async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1429        let username = "user";
1430        let password = "password";
1431        let server = start_test_server(username, password).await;
1432        let base_url = Url::parse(&server.uri())?;
1433
1434        let client = test_client_builder()
1435            .with(
1436                AuthMiddleware::new()
1437                    .with_cache(CredentialsCache::new())
1438                    .with_keyring(Some(KeyringProvider::dummy([(
1439                        // Omit the port from the keyring entry
1440                        base_url.host_str().unwrap(),
1441                        username,
1442                        password,
1443                    )]))),
1444            )
1445            .build();
1446
1447        let mut url = base_url.clone();
1448        url.set_username(username).unwrap();
1449        assert_eq!(
1450            client.get(url).send().await?.status(),
1451            401,
1452            "We should fail because the port is not present in the keyring entry"
1453        );
1454
1455        Ok(())
1456    }
1457
1458    #[test(tokio::test)]
1459    async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1460        let username = "user";
1461        let password = "password";
1462
1463        let server = start_test_server(username, password).await;
1464        let base_url = Url::parse(&server.uri())?;
1465        let cache = CredentialsCache::new();
1466
1467        // Seed _just_ the username. We should pull the username from the cache if not present on the
1468        // URL.
1469        cache.insert(
1470            DisplaySafeUrl::ref_cast(&base_url),
1471            Arc::new(Authentication::from(Credentials::basic(
1472                Some(username.to_string()),
1473                None,
1474            ))),
1475        );
1476        let client = test_client_builder()
1477            .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1478                KeyringProvider::dummy([(
1479                    format!(
1480                        "{}:{}",
1481                        base_url.host_str().unwrap(),
1482                        base_url.port().unwrap()
1483                    ),
1484                    username,
1485                    password,
1486                )]),
1487            )))
1488            .build();
1489
1490        assert_eq!(
1491            client.get(server.uri()).send().await?.status(),
1492            200,
1493            "The username is pulled from the cache, and the password from the keyring"
1494        );
1495
1496        let mut url = base_url.clone();
1497        url.set_username(username).unwrap();
1498        assert_eq!(
1499            client.get(url).send().await?.status(),
1500            200,
1501            "Credentials for the username should be pulled from the keyring"
1502        );
1503
1504        Ok(())
1505    }
1506
1507    #[test(tokio::test)]
1508    async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1509        let username_1 = "user1";
1510        let password_1 = "password1";
1511        let server_1 = start_test_server(username_1, password_1).await;
1512        let base_url_1 = Url::parse(&server_1.uri())?;
1513
1514        let username_2 = "user2";
1515        let password_2 = "password2";
1516        let server_2 = start_test_server(username_2, password_2).await;
1517        let base_url_2 = Url::parse(&server_2.uri())?;
1518
1519        let cache = CredentialsCache::new();
1520        // Seed the cache with our credentials
1521        cache.insert(
1522            DisplaySafeUrl::ref_cast(&base_url_1),
1523            Arc::new(Authentication::from(Credentials::basic(
1524                Some(username_1.to_string()),
1525                Some(password_1.to_string()),
1526            ))),
1527        );
1528        cache.insert(
1529            DisplaySafeUrl::ref_cast(&base_url_2),
1530            Arc::new(Authentication::from(Credentials::basic(
1531                Some(username_2.to_string()),
1532                Some(password_2.to_string()),
1533            ))),
1534        );
1535
1536        let client = test_client_builder()
1537            .with(AuthMiddleware::new().with_cache(cache))
1538            .build();
1539
1540        // Both servers should work
1541        assert_eq!(
1542            client.get(server_1.uri()).send().await?.status(),
1543            200,
1544            "Requests should not require credentials"
1545        );
1546        assert_eq!(
1547            client.get(server_2.uri()).send().await?.status(),
1548            200,
1549            "Requests should not require credentials"
1550        );
1551
1552        assert_eq!(
1553            client
1554                .get(format!("{}/foo", server_1.uri()))
1555                .send()
1556                .await?
1557                .status(),
1558            200,
1559            "Requests can be to different paths in the same realm"
1560        );
1561        assert_eq!(
1562            client
1563                .get(format!("{}/foo", server_2.uri()))
1564                .send()
1565                .await?
1566                .status(),
1567            200,
1568            "Requests can be to different paths in the same realm"
1569        );
1570
1571        Ok(())
1572    }
1573
1574    #[test(tokio::test)]
1575    async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1576        let username_1 = "user1";
1577        let password_1 = "password1";
1578        let server_1 = start_test_server(username_1, password_1).await;
1579        let base_url_1 = Url::parse(&server_1.uri())?;
1580
1581        let username_2 = "user2";
1582        let password_2 = "password2";
1583        let server_2 = start_test_server(username_2, password_2).await;
1584        let base_url_2 = Url::parse(&server_2.uri())?;
1585
1586        let client = test_client_builder()
1587            .with(
1588                AuthMiddleware::new()
1589                    .with_cache(CredentialsCache::new())
1590                    .with_keyring(Some(KeyringProvider::dummy([
1591                        (
1592                            format!(
1593                                "{}:{}",
1594                                base_url_1.host_str().unwrap(),
1595                                base_url_1.port().unwrap()
1596                            ),
1597                            username_1,
1598                            password_1,
1599                        ),
1600                        (
1601                            format!(
1602                                "{}:{}",
1603                                base_url_2.host_str().unwrap(),
1604                                base_url_2.port().unwrap()
1605                            ),
1606                            username_2,
1607                            password_2,
1608                        ),
1609                    ]))),
1610            )
1611            .build();
1612
1613        // Both servers do not work without a username
1614        assert_eq!(
1615            client.get(server_1.uri()).send().await?.status(),
1616            401,
1617            "Requests should require a username"
1618        );
1619        assert_eq!(
1620            client.get(server_2.uri()).send().await?.status(),
1621            401,
1622            "Requests should require a username"
1623        );
1624
1625        let mut url_1 = base_url_1.clone();
1626        url_1.set_username(username_1).unwrap();
1627        assert_eq!(
1628            client.get(url_1.clone()).send().await?.status(),
1629            200,
1630            "Requests with a username should succeed"
1631        );
1632        assert_eq!(
1633            client.get(server_2.uri()).send().await?.status(),
1634            401,
1635            "Credentials should not be re-used for the second server"
1636        );
1637
1638        let mut url_2 = base_url_2.clone();
1639        url_2.set_username(username_2).unwrap();
1640        assert_eq!(
1641            client.get(url_2.clone()).send().await?.status(),
1642            200,
1643            "Requests with a username should succeed"
1644        );
1645
1646        assert_eq!(
1647            client.get(format!("{url_1}/foo")).send().await?.status(),
1648            200,
1649            "Requests can be to different paths in the same realm"
1650        );
1651        assert_eq!(
1652            client.get(format!("{url_2}/foo")).send().await?.status(),
1653            200,
1654            "Requests can be to different paths in the same realm"
1655        );
1656
1657        Ok(())
1658    }
1659
1660    #[test(tokio::test)]
1661    async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1662        let username_1 = "user1";
1663        let password_1 = "password1";
1664        let username_2 = "user2";
1665        let password_2 = "password2";
1666
1667        let server = MockServer::start().await;
1668
1669        Mock::given(method("GET"))
1670            .and(path_regex("/prefix_1.*"))
1671            .and(basic_auth(username_1, password_1))
1672            .respond_with(ResponseTemplate::new(200))
1673            .mount(&server)
1674            .await;
1675
1676        Mock::given(method("GET"))
1677            .and(path_regex("/prefix_2.*"))
1678            .and(basic_auth(username_2, password_2))
1679            .respond_with(ResponseTemplate::new(200))
1680            .mount(&server)
1681            .await;
1682
1683        // Create a third, public prefix
1684        // It will throw a 401 if it receives credentials
1685        Mock::given(method("GET"))
1686            .and(path_regex("/prefix_3.*"))
1687            .and(basic_auth(username_1, password_1))
1688            .respond_with(ResponseTemplate::new(401))
1689            .mount(&server)
1690            .await;
1691        Mock::given(method("GET"))
1692            .and(path_regex("/prefix_3.*"))
1693            .and(basic_auth(username_2, password_2))
1694            .respond_with(ResponseTemplate::new(401))
1695            .mount(&server)
1696            .await;
1697        Mock::given(method("GET"))
1698            .and(path_regex("/prefix_3.*"))
1699            .respond_with(ResponseTemplate::new(200))
1700            .mount(&server)
1701            .await;
1702
1703        Mock::given(method("GET"))
1704            .respond_with(ResponseTemplate::new(401))
1705            .mount(&server)
1706            .await;
1707
1708        let base_url = Url::parse(&server.uri())?;
1709        let base_url_1 = base_url.join("prefix_1")?;
1710        let base_url_2 = base_url.join("prefix_2")?;
1711        let base_url_3 = base_url.join("prefix_3")?;
1712
1713        let cache = CredentialsCache::new();
1714
1715        // Seed the cache with our credentials
1716        cache.insert(
1717            DisplaySafeUrl::ref_cast(&base_url_1),
1718            Arc::new(Authentication::from(Credentials::basic(
1719                Some(username_1.to_string()),
1720                Some(password_1.to_string()),
1721            ))),
1722        );
1723        cache.insert(
1724            DisplaySafeUrl::ref_cast(&base_url_2),
1725            Arc::new(Authentication::from(Credentials::basic(
1726                Some(username_2.to_string()),
1727                Some(password_2.to_string()),
1728            ))),
1729        );
1730
1731        let client = test_client_builder()
1732            .with(AuthMiddleware::new().with_cache(cache))
1733            .build();
1734
1735        // Both servers should work
1736        assert_eq!(
1737            client.get(base_url_1.clone()).send().await?.status(),
1738            200,
1739            "Requests should not require credentials"
1740        );
1741        assert_eq!(
1742            client.get(base_url_2.clone()).send().await?.status(),
1743            200,
1744            "Requests should not require credentials"
1745        );
1746        assert_eq!(
1747            client
1748                .get(base_url.join("prefix_1/foo")?)
1749                .send()
1750                .await?
1751                .status(),
1752            200,
1753            "Requests can be to different paths in the same realm"
1754        );
1755        assert_eq!(
1756            client
1757                .get(base_url.join("prefix_2/foo")?)
1758                .send()
1759                .await?
1760                .status(),
1761            200,
1762            "Requests can be to different paths in the same realm"
1763        );
1764        assert_eq!(
1765            client
1766                .get(base_url.join("prefix_1_foo")?)
1767                .send()
1768                .await?
1769                .status(),
1770            401,
1771            "Requests to paths with a matching prefix but different resource segments should fail"
1772        );
1773
1774        assert_eq!(
1775            client.get(base_url_3.clone()).send().await?.status(),
1776            200,
1777            "Requests to the 'public' prefix should not use credentials"
1778        );
1779
1780        Ok(())
1781    }
1782
1783    #[test(tokio::test)]
1784    async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1785        let username_1 = "user1";
1786        let password_1 = "password1";
1787        let username_2 = "user2";
1788        let password_2 = "password2";
1789
1790        let server = MockServer::start().await;
1791
1792        Mock::given(method("GET"))
1793            .and(path_regex("/prefix_1.*"))
1794            .and(basic_auth(username_1, password_1))
1795            .respond_with(ResponseTemplate::new(200))
1796            .mount(&server)
1797            .await;
1798
1799        Mock::given(method("GET"))
1800            .and(path_regex("/prefix_2.*"))
1801            .and(basic_auth(username_2, password_2))
1802            .respond_with(ResponseTemplate::new(200))
1803            .mount(&server)
1804            .await;
1805
1806        // Create a third, public prefix
1807        // It will throw a 401 if it receives credentials
1808        Mock::given(method("GET"))
1809            .and(path_regex("/prefix_3.*"))
1810            .and(basic_auth(username_1, password_1))
1811            .respond_with(ResponseTemplate::new(401))
1812            .mount(&server)
1813            .await;
1814        Mock::given(method("GET"))
1815            .and(path_regex("/prefix_3.*"))
1816            .and(basic_auth(username_2, password_2))
1817            .respond_with(ResponseTemplate::new(401))
1818            .mount(&server)
1819            .await;
1820        Mock::given(method("GET"))
1821            .and(path_regex("/prefix_3.*"))
1822            .respond_with(ResponseTemplate::new(200))
1823            .mount(&server)
1824            .await;
1825
1826        Mock::given(method("GET"))
1827            .respond_with(ResponseTemplate::new(401))
1828            .mount(&server)
1829            .await;
1830
1831        let base_url = Url::parse(&server.uri())?;
1832        let base_url_1 = base_url.join("prefix_1")?;
1833        let base_url_2 = base_url.join("prefix_2")?;
1834        let base_url_3 = base_url.join("prefix_3")?;
1835
1836        let client = test_client_builder()
1837            .with(
1838                AuthMiddleware::new()
1839                    .with_cache(CredentialsCache::new())
1840                    .with_keyring(Some(KeyringProvider::dummy([
1841                        (
1842                            format!(
1843                                "{}:{}",
1844                                base_url_1.host_str().unwrap(),
1845                                base_url_1.port().unwrap()
1846                            ),
1847                            username_1,
1848                            password_1,
1849                        ),
1850                        (
1851                            format!(
1852                                "{}:{}",
1853                                base_url_2.host_str().unwrap(),
1854                                base_url_2.port().unwrap()
1855                            ),
1856                            username_2,
1857                            password_2,
1858                        ),
1859                    ]))),
1860            )
1861            .build();
1862
1863        // Both servers do not work without a username
1864        assert_eq!(
1865            client.get(base_url_1.clone()).send().await?.status(),
1866            401,
1867            "Requests should require a username"
1868        );
1869        assert_eq!(
1870            client.get(base_url_2.clone()).send().await?.status(),
1871            401,
1872            "Requests should require a username"
1873        );
1874
1875        let mut url_1 = base_url_1.clone();
1876        url_1.set_username(username_1).unwrap();
1877        assert_eq!(
1878            client.get(url_1.clone()).send().await?.status(),
1879            200,
1880            "Requests with a username should succeed"
1881        );
1882        assert_eq!(
1883            client.get(base_url_2.clone()).send().await?.status(),
1884            401,
1885            "Credentials should not be re-used for the second prefix"
1886        );
1887
1888        let mut url_2 = base_url_2.clone();
1889        url_2.set_username(username_2).unwrap();
1890        assert_eq!(
1891            client.get(url_2.clone()).send().await?.status(),
1892            200,
1893            "Requests with a username should succeed"
1894        );
1895
1896        assert_eq!(
1897            client
1898                .get(base_url.join("prefix_1/foo")?)
1899                .send()
1900                .await?
1901                .status(),
1902            200,
1903            "Requests can be to different paths in the same prefix"
1904        );
1905        assert_eq!(
1906            client
1907                .get(base_url.join("prefix_2/foo")?)
1908                .send()
1909                .await?
1910                .status(),
1911            200,
1912            "Requests can be to different paths in the same prefix"
1913        );
1914        assert_eq!(
1915            client
1916                .get(base_url.join("prefix_1_foo")?)
1917                .send()
1918                .await?
1919                .status(),
1920            401,
1921            "Requests to paths with a matching prefix but different resource segments should fail"
1922        );
1923        assert_eq!(
1924            client.get(base_url_3.clone()).send().await?.status(),
1925            200,
1926            "Requests to the 'public' prefix should not use credentials"
1927        );
1928
1929        Ok(())
1930    }
1931
1932    /// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of
1933    /// credentials for _every_ request URL at the cost of inconsistent behavior when
1934    /// credentials are not scoped to a realm.
1935    #[test(tokio::test)]
1936    async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
1937    -> Result<(), Error> {
1938        let username = "user";
1939        let password_1 = "password1";
1940        let password_2 = "password2";
1941
1942        let server = MockServer::start().await;
1943
1944        Mock::given(method("GET"))
1945            .and(path_regex("/prefix_1.*"))
1946            .and(basic_auth(username, password_1))
1947            .respond_with(ResponseTemplate::new(200))
1948            .mount(&server)
1949            .await;
1950
1951        Mock::given(method("GET"))
1952            .and(path_regex("/prefix_2.*"))
1953            .and(basic_auth(username, password_2))
1954            .respond_with(ResponseTemplate::new(200))
1955            .mount(&server)
1956            .await;
1957
1958        Mock::given(method("GET"))
1959            .respond_with(ResponseTemplate::new(401))
1960            .mount(&server)
1961            .await;
1962
1963        let base_url = Url::parse(&server.uri())?;
1964        let base_url_1 = base_url.join("prefix_1")?;
1965        let base_url_2 = base_url.join("prefix_2")?;
1966
1967        let client = test_client_builder()
1968            .with(
1969                AuthMiddleware::new()
1970                    .with_cache(CredentialsCache::new())
1971                    .with_keyring(Some(KeyringProvider::dummy([
1972                        (base_url_1.clone(), username, password_1),
1973                        (base_url_2.clone(), username, password_2),
1974                    ]))),
1975            )
1976            .build();
1977
1978        // Both servers do not work without a username
1979        assert_eq!(
1980            client.get(base_url_1.clone()).send().await?.status(),
1981            401,
1982            "Requests should require a username"
1983        );
1984        assert_eq!(
1985            client.get(base_url_2.clone()).send().await?.status(),
1986            401,
1987            "Requests should require a username"
1988        );
1989
1990        let mut url_1 = base_url_1.clone();
1991        url_1.set_username(username).unwrap();
1992        assert_eq!(
1993            client.get(url_1.clone()).send().await?.status(),
1994            200,
1995            "The first request with a username will succeed"
1996        );
1997        assert_eq!(
1998            client.get(base_url_2.clone()).send().await?.status(),
1999            401,
2000            "Credentials should not be re-used for the second prefix"
2001        );
2002        assert_eq!(
2003            client
2004                .get(base_url.join("prefix_1/foo")?)
2005                .send()
2006                .await?
2007                .status(),
2008            200,
2009            "Subsequent requests can be to different paths in the same prefix"
2010        );
2011
2012        let mut url_2 = base_url_2.clone();
2013        url_2.set_username(username).unwrap();
2014        assert_eq!(
2015            client.get(url_2.clone()).send().await?.status(),
2016            401, // INCORRECT BEHAVIOR
2017            "A request with the same username and realm for a URL that needs a different password will fail"
2018        );
2019        assert_eq!(
2020            client
2021                .get(base_url.join("prefix_2/foo")?)
2022                .send()
2023                .await?
2024                .status(),
2025            401, // INCORRECT BEHAVIOR
2026            "Requests to other paths in the failing prefix will also fail"
2027        );
2028
2029        Ok(())
2030    }
2031
2032    /// Demonstrates that when an index URL is provided, we avoid "incorrect" behavior
2033    /// where multiple URLs with the same username and realm share the same realm-level
2034    /// credentials cache entry.
2035    #[test(tokio::test)]
2036    async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2037    -> Result<(), Error> {
2038        let username = "user";
2039        let password_1 = "password1";
2040        let password_2 = "password2";
2041
2042        let server = MockServer::start().await;
2043
2044        Mock::given(method("GET"))
2045            .and(path_regex("/prefix_1.*"))
2046            .and(basic_auth(username, password_1))
2047            .respond_with(ResponseTemplate::new(200))
2048            .mount(&server)
2049            .await;
2050
2051        Mock::given(method("GET"))
2052            .and(path_regex("/prefix_2.*"))
2053            .and(basic_auth(username, password_2))
2054            .respond_with(ResponseTemplate::new(200))
2055            .mount(&server)
2056            .await;
2057
2058        Mock::given(method("GET"))
2059            .respond_with(ResponseTemplate::new(401))
2060            .mount(&server)
2061            .await;
2062
2063        let base_url = Url::parse(&server.uri())?;
2064        let base_url_1 = base_url.join("prefix_1")?;
2065        let base_url_2 = base_url.join("prefix_2")?;
2066        let indexes = Indexes::from_indexes(vec![
2067            Index {
2068                url: DisplaySafeUrl::from_url(base_url_1.clone()),
2069                root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2070                auth_policy: AuthPolicy::Auto,
2071            },
2072            Index {
2073                url: DisplaySafeUrl::from_url(base_url_2.clone()),
2074                root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2075                auth_policy: AuthPolicy::Auto,
2076            },
2077        ]);
2078
2079        let client = test_client_builder()
2080            .with(
2081                AuthMiddleware::new()
2082                    .with_cache(CredentialsCache::new())
2083                    .with_keyring(Some(KeyringProvider::dummy([
2084                        (base_url_1.clone(), username, password_1),
2085                        (base_url_2.clone(), username, password_2),
2086                    ])))
2087                    .with_indexes(indexes),
2088            )
2089            .build();
2090
2091        // Both servers do not work without a username
2092        assert_eq!(
2093            client.get(base_url_1.clone()).send().await?.status(),
2094            401,
2095            "Requests should require a username"
2096        );
2097        assert_eq!(
2098            client.get(base_url_2.clone()).send().await?.status(),
2099            401,
2100            "Requests should require a username"
2101        );
2102
2103        let mut url_1 = base_url_1.clone();
2104        url_1.set_username(username).unwrap();
2105        assert_eq!(
2106            client.get(url_1.clone()).send().await?.status(),
2107            200,
2108            "The first request with a username will succeed"
2109        );
2110        assert_eq!(
2111            client.get(base_url_2.clone()).send().await?.status(),
2112            401,
2113            "Credentials should not be re-used for the second prefix"
2114        );
2115        assert_eq!(
2116            client
2117                .get(base_url.join("prefix_1/foo")?)
2118                .send()
2119                .await?
2120                .status(),
2121            200,
2122            "Subsequent requests can be to different paths in the same prefix"
2123        );
2124
2125        let mut url_2 = base_url_2.clone();
2126        url_2.set_username(username).unwrap();
2127        assert_eq!(
2128            client.get(url_2.clone()).send().await?.status(),
2129            200,
2130            "A request with the same username and realm for a URL will use index-specific password"
2131        );
2132        assert_eq!(
2133            client
2134                .get(base_url.join("prefix_2/foo")?)
2135                .send()
2136                .await?
2137                .status(),
2138            200,
2139            "Requests to other paths with that prefix will also succeed"
2140        );
2141
2142        Ok(())
2143    }
2144
2145    /// Demonstrates that when an index' credentials are cached for its realm, we
2146    /// find those credentials if they're not present in the keyring.
2147    #[test(tokio::test)]
2148    async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2149    -> Result<(), Error> {
2150        let username = "user";
2151        let password = "password";
2152
2153        let server = MockServer::start().await;
2154
2155        Mock::given(method("GET"))
2156            .and(basic_auth(username, password))
2157            .respond_with(ResponseTemplate::new(200))
2158            .mount(&server)
2159            .await;
2160
2161        Mock::given(method("GET"))
2162            .and(path_regex("/prefix_1.*"))
2163            .and(basic_auth(username, password))
2164            .respond_with(ResponseTemplate::new(200))
2165            .mount(&server)
2166            .await;
2167
2168        Mock::given(method("GET"))
2169            .respond_with(ResponseTemplate::new(401))
2170            .mount(&server)
2171            .await;
2172
2173        let base_url = Url::parse(&server.uri())?;
2174        let index_url = base_url.join("prefix_1")?;
2175        let indexes = Indexes::from_indexes(vec![Index {
2176            url: DisplaySafeUrl::from_url(index_url.clone()),
2177            root_url: DisplaySafeUrl::from_url(index_url.clone()),
2178            auth_policy: AuthPolicy::Auto,
2179        }]);
2180
2181        let client = test_client_builder()
2182            .with(
2183                AuthMiddleware::new()
2184                    .with_cache(CredentialsCache::new())
2185                    .with_keyring(Some(KeyringProvider::dummy([(
2186                        base_url.clone(),
2187                        username,
2188                        password,
2189                    )])))
2190                    .with_indexes(indexes),
2191            )
2192            .build();
2193
2194        // Index server does not work without a username
2195        assert_eq!(
2196            client.get(index_url.clone()).send().await?.status(),
2197            401,
2198            "Requests should require a username"
2199        );
2200
2201        // Send a request that will cache realm credentials.
2202        let mut realm_url = base_url.clone();
2203        realm_url.set_username(username).unwrap();
2204        assert_eq!(
2205            client.get(realm_url.clone()).send().await?.status(),
2206            200,
2207            "The first realm request with a username will succeed"
2208        );
2209
2210        let mut url = index_url.clone();
2211        url.set_username(username).unwrap();
2212        assert_eq!(
2213            client.get(url.clone()).send().await?.status(),
2214            200,
2215            "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2216        );
2217        assert_eq!(
2218            client
2219                .get(base_url.join("prefix_1/foo")?)
2220                .send()
2221                .await?
2222                .status(),
2223            200,
2224            "Requests to other paths with that prefix will also succeed"
2225        );
2226
2227        Ok(())
2228    }
2229
2230    fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2231        let mut url = DisplaySafeUrl::from_url(url.clone());
2232        url.set_password(None).ok();
2233        url.set_username("").ok();
2234        Indexes::from_indexes(vec![Index {
2235            url: url.clone(),
2236            root_url: url.clone(),
2237            auth_policy: policy,
2238        }])
2239    }
2240
2241    /// With the "always" auth policy, requests should succeed on
2242    /// authenticated requests with the correct credentials.
2243    #[test(tokio::test)]
2244    async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2245        let username = "user";
2246        let password = "password";
2247
2248        let server = start_test_server(username, password).await;
2249
2250        let base_url = Url::parse(&server.uri())?;
2251
2252        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2253        let client = test_client_builder()
2254            .with(
2255                AuthMiddleware::new()
2256                    .with_cache(CredentialsCache::new())
2257                    .with_indexes(indexes),
2258            )
2259            .build();
2260
2261        Mock::given(method("GET"))
2262            .and(path_regex("/*"))
2263            .and(basic_auth(username, password))
2264            .respond_with(ResponseTemplate::new(200))
2265            .mount(&server)
2266            .await;
2267
2268        Mock::given(method("GET"))
2269            .respond_with(ResponseTemplate::new(401))
2270            .mount(&server)
2271            .await;
2272
2273        let mut url = base_url.clone();
2274        url.set_username(username).unwrap();
2275        url.set_password(Some(password)).unwrap();
2276        assert_eq!(client.get(url).send().await?.status(), 200);
2277
2278        assert_eq!(
2279            client
2280                .get(format!("{}/foo", server.uri()))
2281                .send()
2282                .await?
2283                .status(),
2284            200,
2285            "Requests can be to different paths with index URL as prefix"
2286        );
2287
2288        let mut url = base_url.clone();
2289        url.set_username(username).unwrap();
2290        url.set_password(Some("invalid")).unwrap();
2291        assert_eq!(
2292            client.get(url).send().await?.status(),
2293            401,
2294            "Incorrect credentials should fail"
2295        );
2296
2297        Ok(())
2298    }
2299
2300    /// With the "always" auth policy, requests should fail if only
2301    /// unauthenticated requests are supported.
2302    #[test(tokio::test)]
2303    async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2304        let server = MockServer::start().await;
2305
2306        Mock::given(method("GET"))
2307            .and(path_regex("/*"))
2308            .respond_with(ResponseTemplate::new(200))
2309            .mount(&server)
2310            .await;
2311
2312        Mock::given(method("GET"))
2313            .respond_with(ResponseTemplate::new(401))
2314            .mount(&server)
2315            .await;
2316
2317        let base_url = Url::parse(&server.uri())?;
2318
2319        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2320        let client = test_client_builder()
2321            .with(
2322                AuthMiddleware::new()
2323                    .with_cache(CredentialsCache::new())
2324                    .with_indexes(indexes),
2325            )
2326            .build();
2327
2328        // Unauthenticated requests are not allowed.
2329        assert_matches!(
2330            client.get(server.uri()).send().await,
2331            Err(reqwest_middleware::Error::Middleware(_))
2332        );
2333
2334        Ok(())
2335    }
2336
2337    /// With the "never" auth policy, requests should fail if
2338    /// an endpoint requires authentication.
2339    #[test(tokio::test)]
2340    async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2341        let username = "user";
2342        let password = "password";
2343
2344        let server = start_test_server(username, password).await;
2345        let base_url = Url::parse(&server.uri())?;
2346
2347        Mock::given(method("GET"))
2348            .and(path_regex("/*"))
2349            .and(basic_auth(username, password))
2350            .respond_with(ResponseTemplate::new(200))
2351            .mount(&server)
2352            .await;
2353
2354        Mock::given(method("GET"))
2355            .respond_with(ResponseTemplate::new(401))
2356            .mount(&server)
2357            .await;
2358
2359        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2360        let client = test_client_builder()
2361            .with(
2362                AuthMiddleware::new()
2363                    .with_cache(CredentialsCache::new())
2364                    .with_indexes(indexes),
2365            )
2366            .build();
2367
2368        let mut url = base_url.clone();
2369        url.set_username(username).unwrap();
2370        url.set_password(Some(password)).unwrap();
2371
2372        assert_eq!(
2373            client
2374                .get(format!("{}/foo", server.uri()))
2375                .send()
2376                .await?
2377                .status(),
2378            401,
2379            "Requests should not be completed if credentials are required"
2380        );
2381
2382        Ok(())
2383    }
2384
2385    /// With the "never" auth policy, requests should succeed if
2386    /// unauthenticated requests succeed.
2387    #[test(tokio::test)]
2388    async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2389        let server = MockServer::start().await;
2390
2391        Mock::given(method("GET"))
2392            .and(path_regex("/*"))
2393            .respond_with(ResponseTemplate::new(200))
2394            .mount(&server)
2395            .await;
2396
2397        Mock::given(method("GET"))
2398            .respond_with(ResponseTemplate::new(401))
2399            .mount(&server)
2400            .await;
2401
2402        let base_url = Url::parse(&server.uri())?;
2403
2404        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2405        let client = test_client_builder()
2406            .with(
2407                AuthMiddleware::new()
2408                    .with_cache(CredentialsCache::new())
2409                    .with_indexes(indexes),
2410            )
2411            .build();
2412
2413        assert_eq!(
2414            client.get(server.uri()).send().await?.status(),
2415            200,
2416            "Requests should succeed if unauthenticated requests can succeed"
2417        );
2418
2419        Ok(())
2420    }
2421
2422    #[test]
2423    fn test_tracing_url() {
2424        // No credentials
2425        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2426        assert_eq!(
2427            tracing_url(&req, None),
2428            DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2429        );
2430
2431        let creds = Authentication::from(Credentials::Basic {
2432            username: Username::new(Some(String::from("user"))),
2433            password: None,
2434        });
2435        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2436        assert_eq!(
2437            tracing_url(&req, Some(&creds)),
2438            DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2439        );
2440
2441        let creds = Authentication::from(Credentials::Basic {
2442            username: Username::new(Some(String::from("user"))),
2443            password: Some(Password::new(String::from("password"))),
2444        });
2445        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2446        assert_eq!(
2447            tracing_url(&req, Some(&creds)),
2448            DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2449                .unwrap()
2450        );
2451    }
2452
2453    #[test(tokio::test)]
2454    async fn test_text_store_basic_auth() -> Result<(), Error> {
2455        let username = "user";
2456        let password = "password";
2457
2458        let server = start_test_server(username, password).await;
2459        let base_url = Url::parse(&server.uri())?;
2460
2461        // Create a text credential store with matching credentials
2462        let mut store = TextCredentialStore::default();
2463        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2464        let credentials =
2465            Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2466        store.insert(service.clone(), credentials);
2467
2468        let client = test_client_builder()
2469            .with(
2470                AuthMiddleware::new()
2471                    .with_cache(CredentialsCache::new())
2472                    .with_text_store(Some(store)),
2473            )
2474            .build();
2475
2476        assert_eq!(
2477            client.get(server.uri()).send().await?.status(),
2478            200,
2479            "Credentials should be pulled from the text store"
2480        );
2481
2482        Ok(())
2483    }
2484
2485    #[test(tokio::test)]
2486    async fn test_text_store_disabled() -> Result<(), Error> {
2487        let username = "user";
2488        let password = "password";
2489        let server = start_test_server(username, password).await;
2490
2491        let client = test_client_builder()
2492            .with(
2493                AuthMiddleware::new()
2494                    .with_cache(CredentialsCache::new())
2495                    .with_text_store(None), // Explicitly disable text store
2496            )
2497            .build();
2498
2499        assert_eq!(
2500            client.get(server.uri()).send().await?.status(),
2501            401,
2502            "Credentials should not be found when text store is disabled"
2503        );
2504
2505        Ok(())
2506    }
2507
2508    #[test(tokio::test)]
2509    async fn test_text_store_by_username() -> Result<(), Error> {
2510        let username = "testuser";
2511        let password = "testpass";
2512        let wrong_username = "wronguser";
2513
2514        let server = start_test_server(username, password).await;
2515        let base_url = Url::parse(&server.uri())?;
2516
2517        let mut store = TextCredentialStore::default();
2518        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2519        let credentials =
2520            crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2521        store.insert(service.clone(), credentials);
2522
2523        let client = test_client_builder()
2524            .with(
2525                AuthMiddleware::new()
2526                    .with_cache(CredentialsCache::new())
2527                    .with_text_store(Some(store)),
2528            )
2529            .build();
2530
2531        // Request with matching username should succeed
2532        let url_with_username = format!(
2533            "{}://{}@{}",
2534            base_url.scheme(),
2535            username,
2536            base_url.host_str().unwrap()
2537        );
2538        let url_with_port = if let Some(port) = base_url.port() {
2539            format!("{}:{}{}", url_with_username, port, base_url.path())
2540        } else {
2541            format!("{}{}", url_with_username, base_url.path())
2542        };
2543
2544        assert_eq!(
2545            client.get(&url_with_port).send().await?.status(),
2546            200,
2547            "Request with matching username should succeed"
2548        );
2549
2550        // Request with non-matching username should fail
2551        let url_with_wrong_username = format!(
2552            "{}://{}@{}",
2553            base_url.scheme(),
2554            wrong_username,
2555            base_url.host_str().unwrap()
2556        );
2557        let url_with_port = if let Some(port) = base_url.port() {
2558            format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2559        } else {
2560            format!("{}{}", url_with_wrong_username, base_url.path())
2561        };
2562
2563        assert_eq!(
2564            client.get(&url_with_port).send().await?.status(),
2565            401,
2566            "Request with non-matching username should fail"
2567        );
2568
2569        // Request without username should succeed
2570        assert_eq!(
2571            client.get(server.uri()).send().await?.status(),
2572            200,
2573            "Request with no username should succeed"
2574        );
2575
2576        Ok(())
2577    }
2578
2579    fn create_request(url: &str) -> Request {
2580        Request::new(Method::GET, Url::parse(url).unwrap())
2581    }
2582
2583    /// Test for <https://github.com/astral-sh/uv/issues/17343>
2584    ///
2585    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
2586    /// should be recognized as having credentials and authenticate successfully.
2587    #[test(tokio::test)]
2588    async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2589        let username = "";
2590        let password = "token";
2591
2592        let server = MockServer::start().await;
2593
2594        Mock::given(method("GET"))
2595            .and(basic_auth(username, password))
2596            .respond_with(ResponseTemplate::new(200))
2597            .mount(&server)
2598            .await;
2599
2600        Mock::given(method("GET"))
2601            .respond_with(ResponseTemplate::new(401))
2602            .mount(&server)
2603            .await;
2604
2605        let client = test_client_builder()
2606            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2607            .build();
2608
2609        let base_url = Url::parse(&server.uri())?;
2610
2611        // Test with the URL format `:password@host` (empty username, password present)
2612        let mut url = base_url.clone();
2613        url.set_password(Some(password)).unwrap();
2614        assert_eq!(
2615            client.get(url).send().await?.status(),
2616            200,
2617            "URL with empty username but password should authenticate successfully"
2618        );
2619
2620        // Subsequent requests to the same realm should also succeed (credentials cached)
2621        assert_eq!(
2622            client.get(server.uri()).send().await?.status(),
2623            200,
2624            "Subsequent requests should use cached credentials"
2625        );
2626
2627        assert_eq!(
2628            client
2629                .get(format!("{}/foo", server.uri()))
2630                .send()
2631                .await?
2632                .status(),
2633            200,
2634            "Requests to different paths in the same realm should succeed"
2635        );
2636
2637        Ok(())
2638    }
2639}