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