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