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