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