uv_auth/
middleware.rs

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