Skip to main content

uv_auth/
middleware.rs

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