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, PreviewFeatures};
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            text_store
807                .get_credentials(
808                    url,
809                    credentials
810                        .as_ref()
811                        .and_then(|credentials| credentials.username()),
812                )
813                .cloned()
814        }) {
815            debug!("Found credentials in plaintext store for {url}");
816            Some(credentials)
817        } else if let Some(credentials) = {
818            if self.preview.is_enabled(PreviewFeatures::NATIVE_AUTH) {
819                let native_store = KeyringProvider::native();
820                let username = credentials.and_then(|credentials| credentials.username());
821                let display_username = if let Some(username) = username {
822                    format!("{username}@")
823                } else {
824                    String::new()
825                };
826                if let Some(index) = index {
827                    // N.B. The native store performs an exact look up right now, so we use the root
828                    // URL of the index instead of relying on prefix-matching.
829                    debug!(
830                        "Checking native store for credentials for index URL {}{}",
831                        display_username, index.root_url
832                    );
833                    native_store.fetch(&index.root_url, username).await
834                } else {
835                    debug!(
836                        "Checking native store for credentials for URL {}{}",
837                        display_username, url
838                    );
839                    native_store.fetch(url, username).await
840                }
841                // TODO(zanieb): We should have a realm fallback here too
842            } else {
843                None
844            }
845        } {
846            debug!("Found credentials in native store for {url}");
847            Some(credentials)
848        // N.B. The keyring provider performs lookups for the exact URL then falls back to the host.
849        //      But, in the absence of an index URL, we cache the result per realm. So in that case,
850        //      if a keyring implementation returns different credentials for different URLs in the
851        //      same realm we will use the wrong credentials.
852        } else if let Some(credentials) = match self.keyring {
853            Some(ref keyring) => {
854                // The subprocess keyring provider is _slow_ so we do not perform fetches for all
855                // URLs; instead, we fetch if there's a username or if the user has requested to
856                // always authenticate.
857                if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
858                    if let Some(index) = index {
859                        debug!(
860                            "Checking keyring for credentials for index URL {}@{}",
861                            username, index.url
862                        );
863                        keyring
864                            .fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
865                            .await
866                    } else {
867                        debug!(
868                            "Checking keyring for credentials for full URL {}@{}",
869                            username, url
870                        );
871                        keyring.fetch(url, Some(username)).await
872                    }
873                } else if matches!(auth_policy, AuthPolicy::Always) {
874                    if let Some(index) = index {
875                        debug!(
876                            "Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
877                            index.url
878                        );
879                        keyring
880                            .fetch(DisplaySafeUrl::ref_cast(&index.url), None)
881                            .await
882                    } else {
883                        None
884                    }
885                } else {
886                    debug!(
887                        "Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
888                    );
889                    None
890                }
891            }
892            None => None,
893        } {
894            debug!("Found credentials in keyring for {url}");
895            Some(credentials)
896        } else {
897            None
898        };
899
900        let credentials = credentials.map(Authentication::from).map(Arc::new);
901
902        // Register the fetch for this key
903        self.cache().fetches.done(key, credentials.clone());
904
905        credentials
906    }
907}
908
909fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl {
910    let mut url = DisplaySafeUrl::from_url(request.url().clone());
911    if let Some(Authentication::Credentials(creds)) = credentials {
912        if let Some(username) = creds.username() {
913            let _ = url.set_username(username);
914        }
915        if let Some(password) = creds.password() {
916            let _ = url.set_password(Some(password));
917        }
918    }
919    url
920}
921
922#[cfg(test)]
923mod tests {
924    use std::io::Write;
925
926    use http::Method;
927    use reqwest::Client;
928    use tempfile::NamedTempFile;
929    use test_log::test;
930
931    use url::Url;
932    use wiremock::matchers::{basic_auth, method, path_regex};
933    use wiremock::{Mock, MockServer, ResponseTemplate};
934
935    use crate::Index;
936    use crate::credentials::Password;
937
938    use super::*;
939
940    type Error = Box<dyn std::error::Error>;
941
942    async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
943        let server = MockServer::start().await;
944
945        Mock::given(method("GET"))
946            .and(basic_auth(username, password))
947            .respond_with(ResponseTemplate::new(200))
948            .mount(&server)
949            .await;
950
951        Mock::given(method("GET"))
952            .respond_with(ResponseTemplate::new(401))
953            .mount(&server)
954            .await;
955
956        server
957    }
958
959    fn test_client_builder() -> reqwest_middleware::ClientBuilder {
960        reqwest_middleware::ClientBuilder::new(
961            Client::builder()
962                .build()
963                .expect("Reqwest client should build"),
964        )
965    }
966
967    #[test(tokio::test)]
968    async fn test_no_credentials() -> Result<(), Error> {
969        let server = start_test_server("user", "password").await;
970        let client = test_client_builder()
971            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
972            .build();
973
974        assert_eq!(
975            client
976                .get(format!("{}/foo", server.uri()))
977                .send()
978                .await?
979                .status(),
980            401
981        );
982
983        assert_eq!(
984            client
985                .get(format!("{}/bar", server.uri()))
986                .send()
987                .await?
988                .status(),
989            401
990        );
991
992        Ok(())
993    }
994
995    /// Without seeding the cache, authenticated requests are not cached
996    #[test(tokio::test)]
997    async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
998        let username = "user";
999        let password = "password";
1000
1001        let server = start_test_server(username, password).await;
1002        let client = test_client_builder()
1003            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1004            .build();
1005
1006        let base_url = Url::parse(&server.uri())?;
1007
1008        let mut url = base_url.clone();
1009        url.set_username(username).unwrap();
1010        url.set_password(Some(password)).unwrap();
1011        assert_eq!(client.get(url).send().await?.status(), 200);
1012
1013        // Works for a URL without credentials now
1014        assert_eq!(
1015            client.get(server.uri()).send().await?.status(),
1016            200,
1017            "Subsequent requests should not require credentials"
1018        );
1019
1020        assert_eq!(
1021            client
1022                .get(format!("{}/foo", server.uri()))
1023                .send()
1024                .await?
1025                .status(),
1026            200,
1027            "Requests can be to different paths in the same realm"
1028        );
1029
1030        let mut url = base_url.clone();
1031        url.set_username(username).unwrap();
1032        url.set_password(Some("invalid")).unwrap();
1033        assert_eq!(
1034            client.get(url).send().await?.status(),
1035            401,
1036            "Credentials in the URL should take precedence and fail"
1037        );
1038
1039        Ok(())
1040    }
1041
1042    #[test(tokio::test)]
1043    async fn test_credentials_in_url_seed() -> Result<(), Error> {
1044        let username = "user";
1045        let password = "password";
1046
1047        let server = start_test_server(username, password).await;
1048        let base_url = Url::parse(&server.uri())?;
1049        let cache = CredentialsCache::new();
1050        cache.insert(
1051            &base_url,
1052            Arc::new(Authentication::from(Credentials::basic(
1053                Some(username.to_string()),
1054                Some(password.to_string()),
1055            ))),
1056        );
1057
1058        let client = test_client_builder()
1059            .with(AuthMiddleware::new().with_cache(cache))
1060            .build();
1061
1062        let mut url = base_url.clone();
1063        url.set_username(username).unwrap();
1064        url.set_password(Some(password)).unwrap();
1065        assert_eq!(client.get(url).send().await?.status(), 200);
1066
1067        // Works for a URL without credentials too
1068        assert_eq!(
1069            client.get(server.uri()).send().await?.status(),
1070            200,
1071            "Requests should not require credentials"
1072        );
1073
1074        assert_eq!(
1075            client
1076                .get(format!("{}/foo", server.uri()))
1077                .send()
1078                .await?
1079                .status(),
1080            200,
1081            "Requests can be to different paths in the same realm"
1082        );
1083
1084        let mut url = base_url.clone();
1085        url.set_username(username).unwrap();
1086        url.set_password(Some("invalid")).unwrap();
1087        assert_eq!(
1088            client.get(url).send().await?.status(),
1089            401,
1090            "Credentials in the URL should take precedence and fail"
1091        );
1092
1093        Ok(())
1094    }
1095
1096    #[test(tokio::test)]
1097    async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1098        let username = "user";
1099        let password = "";
1100
1101        let server = start_test_server(username, password).await;
1102        let base_url = Url::parse(&server.uri())?;
1103        let cache = CredentialsCache::new();
1104        cache.insert(
1105            &base_url,
1106            Arc::new(Authentication::from(Credentials::basic(
1107                Some(username.to_string()),
1108                None,
1109            ))),
1110        );
1111
1112        let client = test_client_builder()
1113            .with(AuthMiddleware::new().with_cache(cache))
1114            .build();
1115
1116        let mut url = base_url.clone();
1117        url.set_username(username).unwrap();
1118        url.set_password(None).unwrap();
1119        assert_eq!(client.get(url).send().await?.status(), 200);
1120
1121        // Works for a URL without credentials too
1122        assert_eq!(
1123            client.get(server.uri()).send().await?.status(),
1124            200,
1125            "Requests should not require credentials"
1126        );
1127
1128        assert_eq!(
1129            client
1130                .get(format!("{}/foo", server.uri()))
1131                .send()
1132                .await?
1133                .status(),
1134            200,
1135            "Requests can be to different paths in the same realm"
1136        );
1137
1138        let mut url = base_url.clone();
1139        url.set_username(username).unwrap();
1140        url.set_password(Some("invalid")).unwrap();
1141        assert_eq!(
1142            client.get(url).send().await?.status(),
1143            401,
1144            "Credentials in the URL should take precedence and fail"
1145        );
1146
1147        assert_eq!(
1148            client.get(server.uri()).send().await?.status(),
1149            200,
1150            "Subsequent requests should not use the invalid credentials"
1151        );
1152
1153        Ok(())
1154    }
1155
1156    #[test(tokio::test)]
1157    async fn test_netrc_file_default_host() -> Result<(), Error> {
1158        let username = "user";
1159        let password = "password";
1160
1161        let mut netrc_file = NamedTempFile::new()?;
1162        writeln!(netrc_file, "default login {username} password {password}")?;
1163
1164        let server = start_test_server(username, password).await;
1165        let client = test_client_builder()
1166            .with(
1167                AuthMiddleware::new()
1168                    .with_cache(CredentialsCache::new())
1169                    .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1170            )
1171            .build();
1172
1173        assert_eq!(
1174            client.get(server.uri()).send().await?.status(),
1175            200,
1176            "Credentials should be pulled from the netrc file"
1177        );
1178
1179        let mut url = Url::parse(&server.uri())?;
1180        url.set_username(username).unwrap();
1181        url.set_password(Some("invalid")).unwrap();
1182        assert_eq!(
1183            client.get(url).send().await?.status(),
1184            401,
1185            "Credentials in the URL should take precedence and fail"
1186        );
1187
1188        assert_eq!(
1189            client.get(server.uri()).send().await?.status(),
1190            200,
1191            "Subsequent requests should not use the invalid credentials"
1192        );
1193
1194        Ok(())
1195    }
1196
1197    #[test(tokio::test)]
1198    async fn test_netrc_file_matching_host() -> Result<(), Error> {
1199        let username = "user";
1200        let password = "password";
1201        let server = start_test_server(username, password).await;
1202        let base_url = Url::parse(&server.uri())?;
1203
1204        let mut netrc_file = NamedTempFile::new()?;
1205        writeln!(
1206            netrc_file,
1207            r"machine {} login {username} password {password}",
1208            base_url.host_str().unwrap()
1209        )?;
1210
1211        let client = test_client_builder()
1212            .with(
1213                AuthMiddleware::new()
1214                    .with_cache(CredentialsCache::new())
1215                    .with_netrc(Some(
1216                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1217                    )),
1218            )
1219            .build();
1220
1221        assert_eq!(
1222            client.get(server.uri()).send().await?.status(),
1223            200,
1224            "Credentials should be pulled from the netrc file"
1225        );
1226
1227        let mut url = base_url.clone();
1228        url.set_username(username).unwrap();
1229        url.set_password(Some("invalid")).unwrap();
1230        assert_eq!(
1231            client.get(url).send().await?.status(),
1232            401,
1233            "Credentials in the URL should take precedence and fail"
1234        );
1235
1236        assert_eq!(
1237            client.get(server.uri()).send().await?.status(),
1238            200,
1239            "Subsequent requests should not use the invalid credentials"
1240        );
1241
1242        Ok(())
1243    }
1244
1245    #[test(tokio::test)]
1246    async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1247        let username = "user";
1248        let password = "password";
1249        let server = start_test_server(username, password).await;
1250
1251        let mut netrc_file = NamedTempFile::new()?;
1252        writeln!(
1253            netrc_file,
1254            r"machine example.com login {username} password {password}",
1255        )?;
1256
1257        let client = test_client_builder()
1258            .with(
1259                AuthMiddleware::new()
1260                    .with_cache(CredentialsCache::new())
1261                    .with_netrc(Some(
1262                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1263                    )),
1264            )
1265            .build();
1266
1267        assert_eq!(
1268            client.get(server.uri()).send().await?.status(),
1269            401,
1270            "Credentials should not be pulled from the netrc file due to host mismatch"
1271        );
1272
1273        let mut url = Url::parse(&server.uri())?;
1274        url.set_username(username).unwrap();
1275        url.set_password(Some(password)).unwrap();
1276        assert_eq!(
1277            client.get(url).send().await?.status(),
1278            200,
1279            "Credentials in the URL should still work"
1280        );
1281
1282        Ok(())
1283    }
1284
1285    #[test(tokio::test)]
1286    async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1287        let username = "user";
1288        let password = "password";
1289        let server = start_test_server(username, password).await;
1290        let base_url = Url::parse(&server.uri())?;
1291
1292        let mut netrc_file = NamedTempFile::new()?;
1293        writeln!(
1294            netrc_file,
1295            r"machine {} login {username} password {password}",
1296            base_url.host_str().unwrap()
1297        )?;
1298
1299        let client = test_client_builder()
1300            .with(
1301                AuthMiddleware::new()
1302                    .with_cache(CredentialsCache::new())
1303                    .with_netrc(Some(
1304                        Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1305                    )),
1306            )
1307            .build();
1308
1309        let mut url = base_url.clone();
1310        url.set_username("other-user").unwrap();
1311        assert_eq!(
1312            client.get(url).send().await?.status(),
1313            401,
1314            "The netrc password should not be used due to a username mismatch"
1315        );
1316
1317        let mut url = base_url.clone();
1318        url.set_username("user").unwrap();
1319        assert_eq!(
1320            client.get(url).send().await?.status(),
1321            200,
1322            "The netrc password should be used for a matching user"
1323        );
1324
1325        Ok(())
1326    }
1327
1328    #[test(tokio::test)]
1329    async fn test_keyring() -> Result<(), Error> {
1330        let username = "user";
1331        let password = "password";
1332        let server = start_test_server(username, password).await;
1333        let base_url = Url::parse(&server.uri())?;
1334
1335        let client = test_client_builder()
1336            .with(
1337                AuthMiddleware::new()
1338                    .with_cache(CredentialsCache::new())
1339                    .with_keyring(Some(KeyringProvider::dummy([(
1340                        format!(
1341                            "{}:{}",
1342                            base_url.host_str().unwrap(),
1343                            base_url.port().unwrap()
1344                        ),
1345                        username,
1346                        password,
1347                    )]))),
1348            )
1349            .build();
1350
1351        assert_eq!(
1352            client.get(server.uri()).send().await?.status(),
1353            401,
1354            "Credentials are not pulled from the keyring without a username"
1355        );
1356
1357        let mut url = base_url.clone();
1358        url.set_username(username).unwrap();
1359        assert_eq!(
1360            client.get(url).send().await?.status(),
1361            200,
1362            "Credentials for the username should be pulled from the keyring"
1363        );
1364
1365        let mut url = base_url.clone();
1366        url.set_username(username).unwrap();
1367        url.set_password(Some("invalid")).unwrap();
1368        assert_eq!(
1369            client.get(url).send().await?.status(),
1370            401,
1371            "Password in the URL should take precedence and fail"
1372        );
1373
1374        let mut url = base_url.clone();
1375        url.set_username(username).unwrap();
1376        assert_eq!(
1377            client.get(url.clone()).send().await?.status(),
1378            200,
1379            "Subsequent requests should not use the invalid password"
1380        );
1381
1382        let mut url = base_url.clone();
1383        url.set_username("other_user").unwrap();
1384        assert_eq!(
1385            client.get(url).send().await?.status(),
1386            401,
1387            "Credentials are not pulled from the keyring when given another username"
1388        );
1389
1390        Ok(())
1391    }
1392
1393    #[test(tokio::test)]
1394    async fn test_keyring_always_authenticate() -> Result<(), Error> {
1395        let username = "user";
1396        let password = "password";
1397        let server = start_test_server(username, password).await;
1398        let base_url = Url::parse(&server.uri())?;
1399
1400        let indexes = indexes_for(&base_url, AuthPolicy::Always);
1401        let client = test_client_builder()
1402            .with(
1403                AuthMiddleware::new()
1404                    .with_cache(CredentialsCache::new())
1405                    .with_keyring(Some(KeyringProvider::dummy([(
1406                        format!(
1407                            "{}:{}",
1408                            base_url.host_str().unwrap(),
1409                            base_url.port().unwrap()
1410                        ),
1411                        username,
1412                        password,
1413                    )])))
1414                    .with_indexes(indexes),
1415            )
1416            .build();
1417
1418        assert_eq!(
1419            client.get(server.uri()).send().await?.status(),
1420            200,
1421            "Credentials (including a username) should be pulled from the keyring"
1422        );
1423
1424        let mut url = base_url.clone();
1425        url.set_username(username).unwrap();
1426        assert_eq!(
1427            client.get(url).send().await?.status(),
1428            200,
1429            "The password for the username should be pulled from the keyring"
1430        );
1431
1432        let mut url = base_url.clone();
1433        url.set_username(username).unwrap();
1434        url.set_password(Some("invalid")).unwrap();
1435        assert_eq!(
1436            client.get(url).send().await?.status(),
1437            401,
1438            "Password in the URL should take precedence and fail"
1439        );
1440
1441        let mut url = base_url.clone();
1442        url.set_username("other_user").unwrap();
1443        assert!(
1444            matches!(
1445                client.get(url).send().await,
1446                Err(reqwest_middleware::Error::Middleware(_))
1447            ),
1448            "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1449        );
1450
1451        Ok(())
1452    }
1453
1454    /// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`,
1455    /// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`.
1456    /// We don't unit test the latter case because it's possible to collide with a server a developer is
1457    /// actually running.
1458    #[test(tokio::test)]
1459    async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1460        let username = "user";
1461        let password = "password";
1462        let server = start_test_server(username, password).await;
1463        let base_url = Url::parse(&server.uri())?;
1464
1465        let client = test_client_builder()
1466            .with(
1467                AuthMiddleware::new()
1468                    .with_cache(CredentialsCache::new())
1469                    .with_keyring(Some(KeyringProvider::dummy([(
1470                        // Omit the port from the keyring entry
1471                        base_url.host_str().unwrap(),
1472                        username,
1473                        password,
1474                    )]))),
1475            )
1476            .build();
1477
1478        let mut url = base_url.clone();
1479        url.set_username(username).unwrap();
1480        assert_eq!(
1481            client.get(url).send().await?.status(),
1482            401,
1483            "We should fail because the port is not present in the keyring entry"
1484        );
1485
1486        Ok(())
1487    }
1488
1489    #[test(tokio::test)]
1490    async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1491        let username = "user";
1492        let password = "password";
1493
1494        let server = start_test_server(username, password).await;
1495        let base_url = Url::parse(&server.uri())?;
1496        let cache = CredentialsCache::new();
1497
1498        // Seed _just_ the username. We should pull the username from the cache if not present on the
1499        // URL.
1500        cache.insert(
1501            &base_url,
1502            Arc::new(Authentication::from(Credentials::basic(
1503                Some(username.to_string()),
1504                None,
1505            ))),
1506        );
1507        let client = test_client_builder()
1508            .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1509                KeyringProvider::dummy([(
1510                    format!(
1511                        "{}:{}",
1512                        base_url.host_str().unwrap(),
1513                        base_url.port().unwrap()
1514                    ),
1515                    username,
1516                    password,
1517                )]),
1518            )))
1519            .build();
1520
1521        assert_eq!(
1522            client.get(server.uri()).send().await?.status(),
1523            200,
1524            "The username is pulled from the cache, and the password from the keyring"
1525        );
1526
1527        let mut url = base_url.clone();
1528        url.set_username(username).unwrap();
1529        assert_eq!(
1530            client.get(url).send().await?.status(),
1531            200,
1532            "Credentials for the username should be pulled from the keyring"
1533        );
1534
1535        Ok(())
1536    }
1537
1538    #[test(tokio::test)]
1539    async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1540        let username_1 = "user1";
1541        let password_1 = "password1";
1542        let server_1 = start_test_server(username_1, password_1).await;
1543        let base_url_1 = Url::parse(&server_1.uri())?;
1544
1545        let username_2 = "user2";
1546        let password_2 = "password2";
1547        let server_2 = start_test_server(username_2, password_2).await;
1548        let base_url_2 = Url::parse(&server_2.uri())?;
1549
1550        let cache = CredentialsCache::new();
1551        // Seed the cache with our credentials
1552        cache.insert(
1553            &base_url_1,
1554            Arc::new(Authentication::from(Credentials::basic(
1555                Some(username_1.to_string()),
1556                Some(password_1.to_string()),
1557            ))),
1558        );
1559        cache.insert(
1560            &base_url_2,
1561            Arc::new(Authentication::from(Credentials::basic(
1562                Some(username_2.to_string()),
1563                Some(password_2.to_string()),
1564            ))),
1565        );
1566
1567        let client = test_client_builder()
1568            .with(AuthMiddleware::new().with_cache(cache))
1569            .build();
1570
1571        // Both servers should work
1572        assert_eq!(
1573            client.get(server_1.uri()).send().await?.status(),
1574            200,
1575            "Requests should not require credentials"
1576        );
1577        assert_eq!(
1578            client.get(server_2.uri()).send().await?.status(),
1579            200,
1580            "Requests should not require credentials"
1581        );
1582
1583        assert_eq!(
1584            client
1585                .get(format!("{}/foo", server_1.uri()))
1586                .send()
1587                .await?
1588                .status(),
1589            200,
1590            "Requests can be to different paths in the same realm"
1591        );
1592        assert_eq!(
1593            client
1594                .get(format!("{}/foo", server_2.uri()))
1595                .send()
1596                .await?
1597                .status(),
1598            200,
1599            "Requests can be to different paths in the same realm"
1600        );
1601
1602        Ok(())
1603    }
1604
1605    #[test(tokio::test)]
1606    async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1607        let username_1 = "user1";
1608        let password_1 = "password1";
1609        let server_1 = start_test_server(username_1, password_1).await;
1610        let base_url_1 = Url::parse(&server_1.uri())?;
1611
1612        let username_2 = "user2";
1613        let password_2 = "password2";
1614        let server_2 = start_test_server(username_2, password_2).await;
1615        let base_url_2 = Url::parse(&server_2.uri())?;
1616
1617        let client = test_client_builder()
1618            .with(
1619                AuthMiddleware::new()
1620                    .with_cache(CredentialsCache::new())
1621                    .with_keyring(Some(KeyringProvider::dummy([
1622                        (
1623                            format!(
1624                                "{}:{}",
1625                                base_url_1.host_str().unwrap(),
1626                                base_url_1.port().unwrap()
1627                            ),
1628                            username_1,
1629                            password_1,
1630                        ),
1631                        (
1632                            format!(
1633                                "{}:{}",
1634                                base_url_2.host_str().unwrap(),
1635                                base_url_2.port().unwrap()
1636                            ),
1637                            username_2,
1638                            password_2,
1639                        ),
1640                    ]))),
1641            )
1642            .build();
1643
1644        // Both servers do not work without a username
1645        assert_eq!(
1646            client.get(server_1.uri()).send().await?.status(),
1647            401,
1648            "Requests should require a username"
1649        );
1650        assert_eq!(
1651            client.get(server_2.uri()).send().await?.status(),
1652            401,
1653            "Requests should require a username"
1654        );
1655
1656        let mut url_1 = base_url_1.clone();
1657        url_1.set_username(username_1).unwrap();
1658        assert_eq!(
1659            client.get(url_1.clone()).send().await?.status(),
1660            200,
1661            "Requests with a username should succeed"
1662        );
1663        assert_eq!(
1664            client.get(server_2.uri()).send().await?.status(),
1665            401,
1666            "Credentials should not be re-used for the second server"
1667        );
1668
1669        let mut url_2 = base_url_2.clone();
1670        url_2.set_username(username_2).unwrap();
1671        assert_eq!(
1672            client.get(url_2.clone()).send().await?.status(),
1673            200,
1674            "Requests with a username should succeed"
1675        );
1676
1677        assert_eq!(
1678            client.get(format!("{url_1}/foo")).send().await?.status(),
1679            200,
1680            "Requests can be to different paths in the same realm"
1681        );
1682        assert_eq!(
1683            client.get(format!("{url_2}/foo")).send().await?.status(),
1684            200,
1685            "Requests can be to different paths in the same realm"
1686        );
1687
1688        Ok(())
1689    }
1690
1691    #[test(tokio::test)]
1692    async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1693        let username_1 = "user1";
1694        let password_1 = "password1";
1695        let username_2 = "user2";
1696        let password_2 = "password2";
1697
1698        let server = MockServer::start().await;
1699
1700        Mock::given(method("GET"))
1701            .and(path_regex("/prefix_1.*"))
1702            .and(basic_auth(username_1, password_1))
1703            .respond_with(ResponseTemplate::new(200))
1704            .mount(&server)
1705            .await;
1706
1707        Mock::given(method("GET"))
1708            .and(path_regex("/prefix_2.*"))
1709            .and(basic_auth(username_2, password_2))
1710            .respond_with(ResponseTemplate::new(200))
1711            .mount(&server)
1712            .await;
1713
1714        // Create a third, public prefix
1715        // It will throw a 401 if it receives credentials
1716        Mock::given(method("GET"))
1717            .and(path_regex("/prefix_3.*"))
1718            .and(basic_auth(username_1, password_1))
1719            .respond_with(ResponseTemplate::new(401))
1720            .mount(&server)
1721            .await;
1722        Mock::given(method("GET"))
1723            .and(path_regex("/prefix_3.*"))
1724            .and(basic_auth(username_2, password_2))
1725            .respond_with(ResponseTemplate::new(401))
1726            .mount(&server)
1727            .await;
1728        Mock::given(method("GET"))
1729            .and(path_regex("/prefix_3.*"))
1730            .respond_with(ResponseTemplate::new(200))
1731            .mount(&server)
1732            .await;
1733
1734        Mock::given(method("GET"))
1735            .respond_with(ResponseTemplate::new(401))
1736            .mount(&server)
1737            .await;
1738
1739        let base_url = Url::parse(&server.uri())?;
1740        let base_url_1 = base_url.join("prefix_1")?;
1741        let base_url_2 = base_url.join("prefix_2")?;
1742        let base_url_3 = base_url.join("prefix_3")?;
1743
1744        let cache = CredentialsCache::new();
1745
1746        // Seed the cache with our credentials
1747        cache.insert(
1748            &base_url_1,
1749            Arc::new(Authentication::from(Credentials::basic(
1750                Some(username_1.to_string()),
1751                Some(password_1.to_string()),
1752            ))),
1753        );
1754        cache.insert(
1755            &base_url_2,
1756            Arc::new(Authentication::from(Credentials::basic(
1757                Some(username_2.to_string()),
1758                Some(password_2.to_string()),
1759            ))),
1760        );
1761
1762        let client = test_client_builder()
1763            .with(AuthMiddleware::new().with_cache(cache))
1764            .build();
1765
1766        // Both servers should work
1767        assert_eq!(
1768            client.get(base_url_1.clone()).send().await?.status(),
1769            200,
1770            "Requests should not require credentials"
1771        );
1772        assert_eq!(
1773            client.get(base_url_2.clone()).send().await?.status(),
1774            200,
1775            "Requests should not require credentials"
1776        );
1777        assert_eq!(
1778            client
1779                .get(base_url.join("prefix_1/foo")?)
1780                .send()
1781                .await?
1782                .status(),
1783            200,
1784            "Requests can be to different paths in the same realm"
1785        );
1786        assert_eq!(
1787            client
1788                .get(base_url.join("prefix_2/foo")?)
1789                .send()
1790                .await?
1791                .status(),
1792            200,
1793            "Requests can be to different paths in the same realm"
1794        );
1795        assert_eq!(
1796            client
1797                .get(base_url.join("prefix_1_foo")?)
1798                .send()
1799                .await?
1800                .status(),
1801            401,
1802            "Requests to paths with a matching prefix but different resource segments should fail"
1803        );
1804
1805        assert_eq!(
1806            client.get(base_url_3.clone()).send().await?.status(),
1807            200,
1808            "Requests to the 'public' prefix should not use credentials"
1809        );
1810
1811        Ok(())
1812    }
1813
1814    #[test(tokio::test)]
1815    async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1816        let username_1 = "user1";
1817        let password_1 = "password1";
1818        let username_2 = "user2";
1819        let password_2 = "password2";
1820
1821        let server = MockServer::start().await;
1822
1823        Mock::given(method("GET"))
1824            .and(path_regex("/prefix_1.*"))
1825            .and(basic_auth(username_1, password_1))
1826            .respond_with(ResponseTemplate::new(200))
1827            .mount(&server)
1828            .await;
1829
1830        Mock::given(method("GET"))
1831            .and(path_regex("/prefix_2.*"))
1832            .and(basic_auth(username_2, password_2))
1833            .respond_with(ResponseTemplate::new(200))
1834            .mount(&server)
1835            .await;
1836
1837        // Create a third, public prefix
1838        // It will throw a 401 if it receives credentials
1839        Mock::given(method("GET"))
1840            .and(path_regex("/prefix_3.*"))
1841            .and(basic_auth(username_1, password_1))
1842            .respond_with(ResponseTemplate::new(401))
1843            .mount(&server)
1844            .await;
1845        Mock::given(method("GET"))
1846            .and(path_regex("/prefix_3.*"))
1847            .and(basic_auth(username_2, password_2))
1848            .respond_with(ResponseTemplate::new(401))
1849            .mount(&server)
1850            .await;
1851        Mock::given(method("GET"))
1852            .and(path_regex("/prefix_3.*"))
1853            .respond_with(ResponseTemplate::new(200))
1854            .mount(&server)
1855            .await;
1856
1857        Mock::given(method("GET"))
1858            .respond_with(ResponseTemplate::new(401))
1859            .mount(&server)
1860            .await;
1861
1862        let base_url = Url::parse(&server.uri())?;
1863        let base_url_1 = base_url.join("prefix_1")?;
1864        let base_url_2 = base_url.join("prefix_2")?;
1865        let base_url_3 = base_url.join("prefix_3")?;
1866
1867        let client = test_client_builder()
1868            .with(
1869                AuthMiddleware::new()
1870                    .with_cache(CredentialsCache::new())
1871                    .with_keyring(Some(KeyringProvider::dummy([
1872                        (
1873                            format!(
1874                                "{}:{}",
1875                                base_url_1.host_str().unwrap(),
1876                                base_url_1.port().unwrap()
1877                            ),
1878                            username_1,
1879                            password_1,
1880                        ),
1881                        (
1882                            format!(
1883                                "{}:{}",
1884                                base_url_2.host_str().unwrap(),
1885                                base_url_2.port().unwrap()
1886                            ),
1887                            username_2,
1888                            password_2,
1889                        ),
1890                    ]))),
1891            )
1892            .build();
1893
1894        // Both servers do not work without a username
1895        assert_eq!(
1896            client.get(base_url_1.clone()).send().await?.status(),
1897            401,
1898            "Requests should require a username"
1899        );
1900        assert_eq!(
1901            client.get(base_url_2.clone()).send().await?.status(),
1902            401,
1903            "Requests should require a username"
1904        );
1905
1906        let mut url_1 = base_url_1.clone();
1907        url_1.set_username(username_1).unwrap();
1908        assert_eq!(
1909            client.get(url_1.clone()).send().await?.status(),
1910            200,
1911            "Requests with a username should succeed"
1912        );
1913        assert_eq!(
1914            client.get(base_url_2.clone()).send().await?.status(),
1915            401,
1916            "Credentials should not be re-used for the second prefix"
1917        );
1918
1919        let mut url_2 = base_url_2.clone();
1920        url_2.set_username(username_2).unwrap();
1921        assert_eq!(
1922            client.get(url_2.clone()).send().await?.status(),
1923            200,
1924            "Requests with a username should succeed"
1925        );
1926
1927        assert_eq!(
1928            client
1929                .get(base_url.join("prefix_1/foo")?)
1930                .send()
1931                .await?
1932                .status(),
1933            200,
1934            "Requests can be to different paths in the same prefix"
1935        );
1936        assert_eq!(
1937            client
1938                .get(base_url.join("prefix_2/foo")?)
1939                .send()
1940                .await?
1941                .status(),
1942            200,
1943            "Requests can be to different paths in the same prefix"
1944        );
1945        assert_eq!(
1946            client
1947                .get(base_url.join("prefix_1_foo")?)
1948                .send()
1949                .await?
1950                .status(),
1951            401,
1952            "Requests to paths with a matching prefix but different resource segments should fail"
1953        );
1954        assert_eq!(
1955            client.get(base_url_3.clone()).send().await?.status(),
1956            200,
1957            "Requests to the 'public' prefix should not use credentials"
1958        );
1959
1960        Ok(())
1961    }
1962
1963    /// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of
1964    /// credentials for _every_ request URL at the cost of inconsistent behavior when
1965    /// credentials are not scoped to a realm.
1966    #[test(tokio::test)]
1967    async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
1968    -> Result<(), Error> {
1969        let username = "user";
1970        let password_1 = "password1";
1971        let password_2 = "password2";
1972
1973        let server = MockServer::start().await;
1974
1975        Mock::given(method("GET"))
1976            .and(path_regex("/prefix_1.*"))
1977            .and(basic_auth(username, password_1))
1978            .respond_with(ResponseTemplate::new(200))
1979            .mount(&server)
1980            .await;
1981
1982        Mock::given(method("GET"))
1983            .and(path_regex("/prefix_2.*"))
1984            .and(basic_auth(username, password_2))
1985            .respond_with(ResponseTemplate::new(200))
1986            .mount(&server)
1987            .await;
1988
1989        Mock::given(method("GET"))
1990            .respond_with(ResponseTemplate::new(401))
1991            .mount(&server)
1992            .await;
1993
1994        let base_url = Url::parse(&server.uri())?;
1995        let base_url_1 = base_url.join("prefix_1")?;
1996        let base_url_2 = base_url.join("prefix_2")?;
1997
1998        let client = test_client_builder()
1999            .with(
2000                AuthMiddleware::new()
2001                    .with_cache(CredentialsCache::new())
2002                    .with_keyring(Some(KeyringProvider::dummy([
2003                        (base_url_1.clone(), username, password_1),
2004                        (base_url_2.clone(), username, password_2),
2005                    ]))),
2006            )
2007            .build();
2008
2009        // Both servers do not work without a username
2010        assert_eq!(
2011            client.get(base_url_1.clone()).send().await?.status(),
2012            401,
2013            "Requests should require a username"
2014        );
2015        assert_eq!(
2016            client.get(base_url_2.clone()).send().await?.status(),
2017            401,
2018            "Requests should require a username"
2019        );
2020
2021        let mut url_1 = base_url_1.clone();
2022        url_1.set_username(username).unwrap();
2023        assert_eq!(
2024            client.get(url_1.clone()).send().await?.status(),
2025            200,
2026            "The first request with a username will succeed"
2027        );
2028        assert_eq!(
2029            client.get(base_url_2.clone()).send().await?.status(),
2030            401,
2031            "Credentials should not be re-used for the second prefix"
2032        );
2033        assert_eq!(
2034            client
2035                .get(base_url.join("prefix_1/foo")?)
2036                .send()
2037                .await?
2038                .status(),
2039            200,
2040            "Subsequent requests can be to different paths in the same prefix"
2041        );
2042
2043        let mut url_2 = base_url_2.clone();
2044        url_2.set_username(username).unwrap();
2045        assert_eq!(
2046            client.get(url_2.clone()).send().await?.status(),
2047            401, // INCORRECT BEHAVIOR
2048            "A request with the same username and realm for a URL that needs a different password will fail"
2049        );
2050        assert_eq!(
2051            client
2052                .get(base_url.join("prefix_2/foo")?)
2053                .send()
2054                .await?
2055                .status(),
2056            401, // INCORRECT BEHAVIOR
2057            "Requests to other paths in the failing prefix will also fail"
2058        );
2059
2060        Ok(())
2061    }
2062
2063    /// Demonstrates that when an index URL is provided, we avoid "incorrect" behavior
2064    /// where multiple URLs with the same username and realm share the same realm-level
2065    /// credentials cache entry.
2066    #[test(tokio::test)]
2067    async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2068    -> Result<(), Error> {
2069        let username = "user";
2070        let password_1 = "password1";
2071        let password_2 = "password2";
2072
2073        let server = MockServer::start().await;
2074
2075        Mock::given(method("GET"))
2076            .and(path_regex("/prefix_1.*"))
2077            .and(basic_auth(username, password_1))
2078            .respond_with(ResponseTemplate::new(200))
2079            .mount(&server)
2080            .await;
2081
2082        Mock::given(method("GET"))
2083            .and(path_regex("/prefix_2.*"))
2084            .and(basic_auth(username, password_2))
2085            .respond_with(ResponseTemplate::new(200))
2086            .mount(&server)
2087            .await;
2088
2089        Mock::given(method("GET"))
2090            .respond_with(ResponseTemplate::new(401))
2091            .mount(&server)
2092            .await;
2093
2094        let base_url = Url::parse(&server.uri())?;
2095        let base_url_1 = base_url.join("prefix_1")?;
2096        let base_url_2 = base_url.join("prefix_2")?;
2097        let indexes = Indexes::from_indexes(vec![
2098            Index {
2099                url: DisplaySafeUrl::from_url(base_url_1.clone()),
2100                root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2101                auth_policy: AuthPolicy::Auto,
2102            },
2103            Index {
2104                url: DisplaySafeUrl::from_url(base_url_2.clone()),
2105                root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2106                auth_policy: AuthPolicy::Auto,
2107            },
2108        ]);
2109
2110        let client = test_client_builder()
2111            .with(
2112                AuthMiddleware::new()
2113                    .with_cache(CredentialsCache::new())
2114                    .with_keyring(Some(KeyringProvider::dummy([
2115                        (base_url_1.clone(), username, password_1),
2116                        (base_url_2.clone(), username, password_2),
2117                    ])))
2118                    .with_indexes(indexes),
2119            )
2120            .build();
2121
2122        // Both servers do not work without a username
2123        assert_eq!(
2124            client.get(base_url_1.clone()).send().await?.status(),
2125            401,
2126            "Requests should require a username"
2127        );
2128        assert_eq!(
2129            client.get(base_url_2.clone()).send().await?.status(),
2130            401,
2131            "Requests should require a username"
2132        );
2133
2134        let mut url_1 = base_url_1.clone();
2135        url_1.set_username(username).unwrap();
2136        assert_eq!(
2137            client.get(url_1.clone()).send().await?.status(),
2138            200,
2139            "The first request with a username will succeed"
2140        );
2141        assert_eq!(
2142            client.get(base_url_2.clone()).send().await?.status(),
2143            401,
2144            "Credentials should not be re-used for the second prefix"
2145        );
2146        assert_eq!(
2147            client
2148                .get(base_url.join("prefix_1/foo")?)
2149                .send()
2150                .await?
2151                .status(),
2152            200,
2153            "Subsequent requests can be to different paths in the same prefix"
2154        );
2155
2156        let mut url_2 = base_url_2.clone();
2157        url_2.set_username(username).unwrap();
2158        assert_eq!(
2159            client.get(url_2.clone()).send().await?.status(),
2160            200,
2161            "A request with the same username and realm for a URL will use index-specific password"
2162        );
2163        assert_eq!(
2164            client
2165                .get(base_url.join("prefix_2/foo")?)
2166                .send()
2167                .await?
2168                .status(),
2169            200,
2170            "Requests to other paths with that prefix will also succeed"
2171        );
2172
2173        Ok(())
2174    }
2175
2176    /// Demonstrates that when an index' credentials are cached for its realm, we
2177    /// find those credentials if they're not present in the keyring.
2178    #[test(tokio::test)]
2179    async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2180    -> Result<(), Error> {
2181        let username = "user";
2182        let password = "password";
2183
2184        let server = MockServer::start().await;
2185
2186        Mock::given(method("GET"))
2187            .and(basic_auth(username, password))
2188            .respond_with(ResponseTemplate::new(200))
2189            .mount(&server)
2190            .await;
2191
2192        Mock::given(method("GET"))
2193            .and(path_regex("/prefix_1.*"))
2194            .and(basic_auth(username, password))
2195            .respond_with(ResponseTemplate::new(200))
2196            .mount(&server)
2197            .await;
2198
2199        Mock::given(method("GET"))
2200            .respond_with(ResponseTemplate::new(401))
2201            .mount(&server)
2202            .await;
2203
2204        let base_url = Url::parse(&server.uri())?;
2205        let index_url = base_url.join("prefix_1")?;
2206        let indexes = Indexes::from_indexes(vec![Index {
2207            url: DisplaySafeUrl::from_url(index_url.clone()),
2208            root_url: DisplaySafeUrl::from_url(index_url.clone()),
2209            auth_policy: AuthPolicy::Auto,
2210        }]);
2211
2212        let client = test_client_builder()
2213            .with(
2214                AuthMiddleware::new()
2215                    .with_cache(CredentialsCache::new())
2216                    .with_keyring(Some(KeyringProvider::dummy([(
2217                        base_url.clone(),
2218                        username,
2219                        password,
2220                    )])))
2221                    .with_indexes(indexes),
2222            )
2223            .build();
2224
2225        // Index server does not work without a username
2226        assert_eq!(
2227            client.get(index_url.clone()).send().await?.status(),
2228            401,
2229            "Requests should require a username"
2230        );
2231
2232        // Send a request that will cache realm credentials.
2233        let mut realm_url = base_url.clone();
2234        realm_url.set_username(username).unwrap();
2235        assert_eq!(
2236            client.get(realm_url.clone()).send().await?.status(),
2237            200,
2238            "The first realm request with a username will succeed"
2239        );
2240
2241        let mut url = index_url.clone();
2242        url.set_username(username).unwrap();
2243        assert_eq!(
2244            client.get(url.clone()).send().await?.status(),
2245            200,
2246            "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2247        );
2248        assert_eq!(
2249            client
2250                .get(base_url.join("prefix_1/foo")?)
2251                .send()
2252                .await?
2253                .status(),
2254            200,
2255            "Requests to other paths with that prefix will also succeed"
2256        );
2257
2258        Ok(())
2259    }
2260
2261    fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2262        let mut url = DisplaySafeUrl::from_url(url.clone());
2263        url.set_password(None).ok();
2264        url.set_username("").ok();
2265        Indexes::from_indexes(vec![Index {
2266            url: url.clone(),
2267            root_url: url.clone(),
2268            auth_policy: policy,
2269        }])
2270    }
2271
2272    /// With the "always" auth policy, requests should succeed on
2273    /// authenticated requests with the correct credentials.
2274    #[test(tokio::test)]
2275    async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2276        let username = "user";
2277        let password = "password";
2278
2279        let server = start_test_server(username, password).await;
2280
2281        let base_url = Url::parse(&server.uri())?;
2282
2283        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2284        let client = test_client_builder()
2285            .with(
2286                AuthMiddleware::new()
2287                    .with_cache(CredentialsCache::new())
2288                    .with_indexes(indexes),
2289            )
2290            .build();
2291
2292        Mock::given(method("GET"))
2293            .and(path_regex("/*"))
2294            .and(basic_auth(username, password))
2295            .respond_with(ResponseTemplate::new(200))
2296            .mount(&server)
2297            .await;
2298
2299        Mock::given(method("GET"))
2300            .respond_with(ResponseTemplate::new(401))
2301            .mount(&server)
2302            .await;
2303
2304        let mut url = base_url.clone();
2305        url.set_username(username).unwrap();
2306        url.set_password(Some(password)).unwrap();
2307        assert_eq!(client.get(url).send().await?.status(), 200);
2308
2309        assert_eq!(
2310            client
2311                .get(format!("{}/foo", server.uri()))
2312                .send()
2313                .await?
2314                .status(),
2315            200,
2316            "Requests can be to different paths with index URL as prefix"
2317        );
2318
2319        let mut url = base_url.clone();
2320        url.set_username(username).unwrap();
2321        url.set_password(Some("invalid")).unwrap();
2322        assert_eq!(
2323            client.get(url).send().await?.status(),
2324            401,
2325            "Incorrect credentials should fail"
2326        );
2327
2328        Ok(())
2329    }
2330
2331    /// With the "always" auth policy, requests should fail if only
2332    /// unauthenticated requests are supported.
2333    #[test(tokio::test)]
2334    async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2335        let server = MockServer::start().await;
2336
2337        Mock::given(method("GET"))
2338            .and(path_regex("/*"))
2339            .respond_with(ResponseTemplate::new(200))
2340            .mount(&server)
2341            .await;
2342
2343        Mock::given(method("GET"))
2344            .respond_with(ResponseTemplate::new(401))
2345            .mount(&server)
2346            .await;
2347
2348        let base_url = Url::parse(&server.uri())?;
2349
2350        let indexes = indexes_for(&base_url, AuthPolicy::Always);
2351        let client = test_client_builder()
2352            .with(
2353                AuthMiddleware::new()
2354                    .with_cache(CredentialsCache::new())
2355                    .with_indexes(indexes),
2356            )
2357            .build();
2358
2359        // Unauthenticated requests are not allowed.
2360        assert!(matches!(
2361            client.get(server.uri()).send().await,
2362            Err(reqwest_middleware::Error::Middleware(_))
2363        ));
2364
2365        Ok(())
2366    }
2367
2368    /// With the "never" auth policy, requests should fail if
2369    /// an endpoint requires authentication.
2370    #[test(tokio::test)]
2371    async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2372        let username = "user";
2373        let password = "password";
2374
2375        let server = start_test_server(username, password).await;
2376        let base_url = Url::parse(&server.uri())?;
2377
2378        Mock::given(method("GET"))
2379            .and(path_regex("/*"))
2380            .and(basic_auth(username, password))
2381            .respond_with(ResponseTemplate::new(200))
2382            .mount(&server)
2383            .await;
2384
2385        Mock::given(method("GET"))
2386            .respond_with(ResponseTemplate::new(401))
2387            .mount(&server)
2388            .await;
2389
2390        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2391        let client = test_client_builder()
2392            .with(
2393                AuthMiddleware::new()
2394                    .with_cache(CredentialsCache::new())
2395                    .with_indexes(indexes),
2396            )
2397            .build();
2398
2399        let mut url = base_url.clone();
2400        url.set_username(username).unwrap();
2401        url.set_password(Some(password)).unwrap();
2402
2403        assert_eq!(
2404            client
2405                .get(format!("{}/foo", server.uri()))
2406                .send()
2407                .await?
2408                .status(),
2409            401,
2410            "Requests should not be completed if credentials are required"
2411        );
2412
2413        Ok(())
2414    }
2415
2416    /// With the "never" auth policy, requests should succeed if
2417    /// unauthenticated requests succeed.
2418    #[test(tokio::test)]
2419    async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2420        let server = MockServer::start().await;
2421
2422        Mock::given(method("GET"))
2423            .and(path_regex("/*"))
2424            .respond_with(ResponseTemplate::new(200))
2425            .mount(&server)
2426            .await;
2427
2428        Mock::given(method("GET"))
2429            .respond_with(ResponseTemplate::new(401))
2430            .mount(&server)
2431            .await;
2432
2433        let base_url = Url::parse(&server.uri())?;
2434
2435        let indexes = indexes_for(&base_url, AuthPolicy::Never);
2436        let client = test_client_builder()
2437            .with(
2438                AuthMiddleware::new()
2439                    .with_cache(CredentialsCache::new())
2440                    .with_indexes(indexes),
2441            )
2442            .build();
2443
2444        assert_eq!(
2445            client.get(server.uri()).send().await?.status(),
2446            200,
2447            "Requests should succeed if unauthenticated requests can succeed"
2448        );
2449
2450        Ok(())
2451    }
2452
2453    #[test]
2454    fn test_tracing_url() {
2455        // No credentials
2456        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2457        assert_eq!(
2458            tracing_url(&req, None),
2459            DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2460        );
2461
2462        let creds = Authentication::from(Credentials::Basic {
2463            username: Username::new(Some(String::from("user"))),
2464            password: None,
2465        });
2466        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2467        assert_eq!(
2468            tracing_url(&req, Some(&creds)),
2469            DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2470        );
2471
2472        let creds = Authentication::from(Credentials::Basic {
2473            username: Username::new(Some(String::from("user"))),
2474            password: Some(Password::new(String::from("password"))),
2475        });
2476        let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2477        assert_eq!(
2478            tracing_url(&req, Some(&creds)),
2479            DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2480                .unwrap()
2481        );
2482    }
2483
2484    #[test(tokio::test)]
2485    async fn test_text_store_basic_auth() -> Result<(), Error> {
2486        let username = "user";
2487        let password = "password";
2488
2489        let server = start_test_server(username, password).await;
2490        let base_url = Url::parse(&server.uri())?;
2491
2492        // Create a text credential store with matching credentials
2493        let mut store = TextCredentialStore::default();
2494        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2495        let credentials =
2496            Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2497        store.insert(service.clone(), credentials);
2498
2499        let client = test_client_builder()
2500            .with(
2501                AuthMiddleware::new()
2502                    .with_cache(CredentialsCache::new())
2503                    .with_text_store(Some(store)),
2504            )
2505            .build();
2506
2507        assert_eq!(
2508            client.get(server.uri()).send().await?.status(),
2509            200,
2510            "Credentials should be pulled from the text store"
2511        );
2512
2513        Ok(())
2514    }
2515
2516    #[test(tokio::test)]
2517    async fn test_text_store_disabled() -> Result<(), Error> {
2518        let username = "user";
2519        let password = "password";
2520        let server = start_test_server(username, password).await;
2521
2522        let client = test_client_builder()
2523            .with(
2524                AuthMiddleware::new()
2525                    .with_cache(CredentialsCache::new())
2526                    .with_text_store(None), // Explicitly disable text store
2527            )
2528            .build();
2529
2530        assert_eq!(
2531            client.get(server.uri()).send().await?.status(),
2532            401,
2533            "Credentials should not be found when text store is disabled"
2534        );
2535
2536        Ok(())
2537    }
2538
2539    #[test(tokio::test)]
2540    async fn test_text_store_by_username() -> Result<(), Error> {
2541        let username = "testuser";
2542        let password = "testpass";
2543        let wrong_username = "wronguser";
2544
2545        let server = start_test_server(username, password).await;
2546        let base_url = Url::parse(&server.uri())?;
2547
2548        let mut store = TextCredentialStore::default();
2549        let service = crate::Service::try_from(base_url.to_string()).unwrap();
2550        let credentials =
2551            crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2552        store.insert(service.clone(), credentials);
2553
2554        let client = test_client_builder()
2555            .with(
2556                AuthMiddleware::new()
2557                    .with_cache(CredentialsCache::new())
2558                    .with_text_store(Some(store)),
2559            )
2560            .build();
2561
2562        // Request with matching username should succeed
2563        let url_with_username = format!(
2564            "{}://{}@{}",
2565            base_url.scheme(),
2566            username,
2567            base_url.host_str().unwrap()
2568        );
2569        let url_with_port = if let Some(port) = base_url.port() {
2570            format!("{}:{}{}", url_with_username, port, base_url.path())
2571        } else {
2572            format!("{}{}", url_with_username, base_url.path())
2573        };
2574
2575        assert_eq!(
2576            client.get(&url_with_port).send().await?.status(),
2577            200,
2578            "Request with matching username should succeed"
2579        );
2580
2581        // Request with non-matching username should fail
2582        let url_with_wrong_username = format!(
2583            "{}://{}@{}",
2584            base_url.scheme(),
2585            wrong_username,
2586            base_url.host_str().unwrap()
2587        );
2588        let url_with_port = if let Some(port) = base_url.port() {
2589            format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2590        } else {
2591            format!("{}{}", url_with_wrong_username, base_url.path())
2592        };
2593
2594        assert_eq!(
2595            client.get(&url_with_port).send().await?.status(),
2596            401,
2597            "Request with non-matching username should fail"
2598        );
2599
2600        // Request without username should succeed
2601        assert_eq!(
2602            client.get(server.uri()).send().await?.status(),
2603            200,
2604            "Request with no username should succeed"
2605        );
2606
2607        Ok(())
2608    }
2609
2610    fn create_request(url: &str) -> Request {
2611        Request::new(Method::GET, Url::parse(url).unwrap())
2612    }
2613
2614    /// Test for <https://github.com/astral-sh/uv/issues/17343>
2615    ///
2616    /// URLs with an empty username but a password (e.g., `https://:token@example.com`)
2617    /// should be recognized as having credentials and authenticate successfully.
2618    #[test(tokio::test)]
2619    async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2620        let username = "";
2621        let password = "token";
2622
2623        let server = MockServer::start().await;
2624
2625        Mock::given(method("GET"))
2626            .and(basic_auth(username, password))
2627            .respond_with(ResponseTemplate::new(200))
2628            .mount(&server)
2629            .await;
2630
2631        Mock::given(method("GET"))
2632            .respond_with(ResponseTemplate::new(401))
2633            .mount(&server)
2634            .await;
2635
2636        let client = test_client_builder()
2637            .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2638            .build();
2639
2640        let base_url = Url::parse(&server.uri())?;
2641
2642        // Test with the URL format `:password@host` (empty username, password present)
2643        let mut url = base_url.clone();
2644        url.set_password(Some(password)).unwrap();
2645        assert_eq!(
2646            client.get(url).send().await?.status(),
2647            200,
2648            "URL with empty username but password should authenticate successfully"
2649        );
2650
2651        // Subsequent requests to the same realm should also succeed (credentials cached)
2652        assert_eq!(
2653            client.get(server.uri()).send().await?.status(),
2654            200,
2655            "Subsequent requests should use cached credentials"
2656        );
2657
2658        assert_eq!(
2659            client
2660                .get(format!("{}/foo", server.uri()))
2661                .send()
2662                .await?
2663                .status(),
2664            200,
2665            "Requests to different paths in the same realm should succeed"
2666        );
2667
2668        Ok(())
2669    }
2670}