Skip to main content

uv_auth/
middleware.rs

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