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