Skip to main content

uv_auth/
middleware.rs

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