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