Skip to main content

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