Skip to main content

uv_auth/
middleware.rs

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