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