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