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