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