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::io::Write;
989
990 use http::Method;
991 use reqwest::Client;
992 use tempfile::NamedTempFile;
993 use test_log::test;
994
995 use url::Url;
996 use wiremock::matchers::{basic_auth, method, path_regex};
997 use wiremock::{Mock, MockServer, ResponseTemplate};
998
999 use crate::Index;
1000 use crate::credentials::Password;
1001
1002 use super::*;
1003
1004 type Error = Box<dyn std::error::Error>;
1005
1006 async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
1007 let server = MockServer::start().await;
1008
1009 Mock::given(method("GET"))
1010 .and(basic_auth(username, password))
1011 .respond_with(ResponseTemplate::new(200))
1012 .mount(&server)
1013 .await;
1014
1015 Mock::given(method("GET"))
1016 .respond_with(ResponseTemplate::new(401))
1017 .mount(&server)
1018 .await;
1019
1020 server
1021 }
1022
1023 fn test_client_builder() -> reqwest_middleware::ClientBuilder {
1024 reqwest_middleware::ClientBuilder::new(
1025 Client::builder()
1026 .build()
1027 .expect("Reqwest client should build"),
1028 )
1029 }
1030
1031 #[test(tokio::test)]
1032 async fn test_no_credentials() -> Result<(), Error> {
1033 let server = start_test_server("user", "password").await;
1034 let client = test_client_builder()
1035 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1036 .build();
1037
1038 assert_eq!(
1039 client
1040 .get(format!("{}/foo", server.uri()))
1041 .send()
1042 .await?
1043 .status(),
1044 401
1045 );
1046
1047 assert_eq!(
1048 client
1049 .get(format!("{}/bar", server.uri()))
1050 .send()
1051 .await?
1052 .status(),
1053 401
1054 );
1055
1056 Ok(())
1057 }
1058
1059 #[test(tokio::test)]
1061 async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
1062 let username = "user";
1063 let password = "password";
1064
1065 let server = start_test_server(username, password).await;
1066 let client = test_client_builder()
1067 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1068 .build();
1069
1070 let base_url = Url::parse(&server.uri())?;
1071
1072 let mut url = base_url.clone();
1073 url.set_username(username).unwrap();
1074 url.set_password(Some(password)).unwrap();
1075 assert_eq!(client.get(url).send().await?.status(), 200);
1076
1077 assert_eq!(
1079 client.get(server.uri()).send().await?.status(),
1080 200,
1081 "Subsequent requests should not require credentials"
1082 );
1083
1084 assert_eq!(
1085 client
1086 .get(format!("{}/foo", server.uri()))
1087 .send()
1088 .await?
1089 .status(),
1090 200,
1091 "Requests can be to different paths in the same realm"
1092 );
1093
1094 let mut url = base_url.clone();
1095 url.set_username(username).unwrap();
1096 url.set_password(Some("invalid")).unwrap();
1097 assert_eq!(
1098 client.get(url).send().await?.status(),
1099 401,
1100 "Credentials in the URL should take precedence and fail"
1101 );
1102
1103 Ok(())
1104 }
1105
1106 #[test(tokio::test)]
1107 async fn test_credentials_in_url_seed() -> Result<(), Error> {
1108 let username = "user";
1109 let password = "password";
1110
1111 let server = start_test_server(username, password).await;
1112 let base_url = Url::parse(&server.uri())?;
1113 let cache = CredentialsCache::new();
1114 cache.insert(
1115 DisplaySafeUrl::ref_cast(&base_url),
1116 Arc::new(Authentication::from(Credentials::basic(
1117 Some(username.to_string()),
1118 Some(password.to_string()),
1119 ))),
1120 );
1121
1122 let client = test_client_builder()
1123 .with(AuthMiddleware::new().with_cache(cache))
1124 .build();
1125
1126 let mut url = base_url.clone();
1127 url.set_username(username).unwrap();
1128 url.set_password(Some(password)).unwrap();
1129 assert_eq!(client.get(url).send().await?.status(), 200);
1130
1131 assert_eq!(
1133 client.get(server.uri()).send().await?.status(),
1134 200,
1135 "Requests should not require credentials"
1136 );
1137
1138 assert_eq!(
1139 client
1140 .get(format!("{}/foo", server.uri()))
1141 .send()
1142 .await?
1143 .status(),
1144 200,
1145 "Requests can be to different paths in the same realm"
1146 );
1147
1148 let mut url = base_url.clone();
1149 url.set_username(username).unwrap();
1150 url.set_password(Some("invalid")).unwrap();
1151 assert_eq!(
1152 client.get(url).send().await?.status(),
1153 401,
1154 "Credentials in the URL should take precedence and fail"
1155 );
1156
1157 Ok(())
1158 }
1159
1160 #[test(tokio::test)]
1161 async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1162 let username = "user";
1163 let password = "";
1164
1165 let server = start_test_server(username, password).await;
1166 let base_url = Url::parse(&server.uri())?;
1167 let cache = CredentialsCache::new();
1168 cache.insert(
1169 DisplaySafeUrl::ref_cast(&base_url),
1170 Arc::new(Authentication::from(Credentials::basic(
1171 Some(username.to_string()),
1172 None,
1173 ))),
1174 );
1175
1176 let client = test_client_builder()
1177 .with(AuthMiddleware::new().with_cache(cache))
1178 .build();
1179
1180 let mut url = base_url.clone();
1181 url.set_username(username).unwrap();
1182 url.set_password(None).unwrap();
1183 assert_eq!(client.get(url).send().await?.status(), 200);
1184
1185 assert_eq!(
1187 client.get(server.uri()).send().await?.status(),
1188 200,
1189 "Requests should not require credentials"
1190 );
1191
1192 assert_eq!(
1193 client
1194 .get(format!("{}/foo", server.uri()))
1195 .send()
1196 .await?
1197 .status(),
1198 200,
1199 "Requests can be to different paths in the same realm"
1200 );
1201
1202 let mut url = base_url.clone();
1203 url.set_username(username).unwrap();
1204 url.set_password(Some("invalid")).unwrap();
1205 assert_eq!(
1206 client.get(url).send().await?.status(),
1207 401,
1208 "Credentials in the URL should take precedence and fail"
1209 );
1210
1211 assert_eq!(
1212 client.get(server.uri()).send().await?.status(),
1213 200,
1214 "Subsequent requests should not use the invalid credentials"
1215 );
1216
1217 Ok(())
1218 }
1219
1220 #[test(tokio::test)]
1221 async fn test_netrc_file_default_host() -> Result<(), Error> {
1222 let username = "user";
1223 let password = "password";
1224
1225 let mut netrc_file = NamedTempFile::new()?;
1226 writeln!(netrc_file, "default login {username} password {password}")?;
1227
1228 let server = start_test_server(username, password).await;
1229 let client = test_client_builder()
1230 .with(
1231 AuthMiddleware::new()
1232 .with_cache(CredentialsCache::new())
1233 .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1234 )
1235 .build();
1236
1237 assert_eq!(
1238 client.get(server.uri()).send().await?.status(),
1239 200,
1240 "Credentials should be pulled from the netrc file"
1241 );
1242
1243 let mut url = Url::parse(&server.uri())?;
1244 url.set_username(username).unwrap();
1245 url.set_password(Some("invalid")).unwrap();
1246 assert_eq!(
1247 client.get(url).send().await?.status(),
1248 401,
1249 "Credentials in the URL should take precedence and fail"
1250 );
1251
1252 assert_eq!(
1253 client.get(server.uri()).send().await?.status(),
1254 200,
1255 "Subsequent requests should not use the invalid credentials"
1256 );
1257
1258 Ok(())
1259 }
1260
1261 #[test(tokio::test)]
1262 async fn test_netrc_file_matching_host() -> Result<(), Error> {
1263 let username = "user";
1264 let password = "password";
1265 let server = start_test_server(username, password).await;
1266 let base_url = Url::parse(&server.uri())?;
1267
1268 let mut netrc_file = NamedTempFile::new()?;
1269 writeln!(
1270 netrc_file,
1271 r"machine {} login {username} password {password}",
1272 base_url.host_str().unwrap()
1273 )?;
1274
1275 let client = test_client_builder()
1276 .with(
1277 AuthMiddleware::new()
1278 .with_cache(CredentialsCache::new())
1279 .with_netrc(Some(
1280 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1281 )),
1282 )
1283 .build();
1284
1285 assert_eq!(
1286 client.get(server.uri()).send().await?.status(),
1287 200,
1288 "Credentials should be pulled from the netrc file"
1289 );
1290
1291 let mut url = base_url.clone();
1292 url.set_username(username).unwrap();
1293 url.set_password(Some("invalid")).unwrap();
1294 assert_eq!(
1295 client.get(url).send().await?.status(),
1296 401,
1297 "Credentials in the URL should take precedence and fail"
1298 );
1299
1300 assert_eq!(
1301 client.get(server.uri()).send().await?.status(),
1302 200,
1303 "Subsequent requests should not use the invalid credentials"
1304 );
1305
1306 Ok(())
1307 }
1308
1309 #[test(tokio::test)]
1310 async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1311 let username = "user";
1312 let password = "password";
1313 let server = start_test_server(username, password).await;
1314
1315 let mut netrc_file = NamedTempFile::new()?;
1316 writeln!(
1317 netrc_file,
1318 r"machine example.com login {username} password {password}",
1319 )?;
1320
1321 let client = test_client_builder()
1322 .with(
1323 AuthMiddleware::new()
1324 .with_cache(CredentialsCache::new())
1325 .with_netrc(Some(
1326 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1327 )),
1328 )
1329 .build();
1330
1331 assert_eq!(
1332 client.get(server.uri()).send().await?.status(),
1333 401,
1334 "Credentials should not be pulled from the netrc file due to host mismatch"
1335 );
1336
1337 let mut url = Url::parse(&server.uri())?;
1338 url.set_username(username).unwrap();
1339 url.set_password(Some(password)).unwrap();
1340 assert_eq!(
1341 client.get(url).send().await?.status(),
1342 200,
1343 "Credentials in the URL should still work"
1344 );
1345
1346 Ok(())
1347 }
1348
1349 #[test(tokio::test)]
1350 async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1351 let username = "user";
1352 let password = "password";
1353 let server = start_test_server(username, password).await;
1354 let base_url = Url::parse(&server.uri())?;
1355
1356 let mut netrc_file = NamedTempFile::new()?;
1357 writeln!(
1358 netrc_file,
1359 r"machine {} login {username} password {password}",
1360 base_url.host_str().unwrap()
1361 )?;
1362
1363 let client = test_client_builder()
1364 .with(
1365 AuthMiddleware::new()
1366 .with_cache(CredentialsCache::new())
1367 .with_netrc(Some(
1368 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1369 )),
1370 )
1371 .build();
1372
1373 let mut url = base_url.clone();
1374 url.set_username("other-user").unwrap();
1375 assert_eq!(
1376 client.get(url).send().await?.status(),
1377 401,
1378 "The netrc password should not be used due to a username mismatch"
1379 );
1380
1381 let mut url = base_url.clone();
1382 url.set_username("user").unwrap();
1383 assert_eq!(
1384 client.get(url).send().await?.status(),
1385 200,
1386 "The netrc password should be used for a matching user"
1387 );
1388
1389 Ok(())
1390 }
1391
1392 #[test(tokio::test)]
1393 async fn test_keyring() -> Result<(), Error> {
1394 let username = "user";
1395 let password = "password";
1396 let server = start_test_server(username, password).await;
1397 let base_url = Url::parse(&server.uri())?;
1398
1399 let client = test_client_builder()
1400 .with(
1401 AuthMiddleware::new()
1402 .with_cache(CredentialsCache::new())
1403 .with_keyring(Some(KeyringProvider::dummy([(
1404 format!(
1405 "{}:{}",
1406 base_url.host_str().unwrap(),
1407 base_url.port().unwrap()
1408 ),
1409 username,
1410 password,
1411 )]))),
1412 )
1413 .build();
1414
1415 assert_eq!(
1416 client.get(server.uri()).send().await?.status(),
1417 401,
1418 "Credentials are not pulled from the keyring without a username"
1419 );
1420
1421 let mut url = base_url.clone();
1422 url.set_username(username).unwrap();
1423 assert_eq!(
1424 client.get(url).send().await?.status(),
1425 200,
1426 "Credentials for the username should be pulled from the keyring"
1427 );
1428
1429 let mut url = base_url.clone();
1430 url.set_username(username).unwrap();
1431 url.set_password(Some("invalid")).unwrap();
1432 assert_eq!(
1433 client.get(url).send().await?.status(),
1434 401,
1435 "Password in the URL should take precedence and fail"
1436 );
1437
1438 let mut url = base_url.clone();
1439 url.set_username(username).unwrap();
1440 assert_eq!(
1441 client.get(url.clone()).send().await?.status(),
1442 200,
1443 "Subsequent requests should not use the invalid password"
1444 );
1445
1446 let mut url = base_url.clone();
1447 url.set_username("other_user").unwrap();
1448 assert_eq!(
1449 client.get(url).send().await?.status(),
1450 401,
1451 "Credentials are not pulled from the keyring when given another username"
1452 );
1453
1454 Ok(())
1455 }
1456
1457 #[test(tokio::test)]
1458 async fn test_keyring_always_authenticate() -> Result<(), Error> {
1459 let username = "user";
1460 let password = "password";
1461 let server = start_test_server(username, password).await;
1462 let base_url = Url::parse(&server.uri())?;
1463
1464 let indexes = indexes_for(&base_url, AuthPolicy::Always);
1465 let client = test_client_builder()
1466 .with(
1467 AuthMiddleware::new()
1468 .with_cache(CredentialsCache::new())
1469 .with_keyring(Some(KeyringProvider::dummy([(
1470 format!(
1471 "{}:{}",
1472 base_url.host_str().unwrap(),
1473 base_url.port().unwrap()
1474 ),
1475 username,
1476 password,
1477 )])))
1478 .with_indexes(indexes),
1479 )
1480 .build();
1481
1482 assert_eq!(
1483 client.get(server.uri()).send().await?.status(),
1484 200,
1485 "Credentials (including a username) should be pulled from the keyring"
1486 );
1487
1488 let mut url = base_url.clone();
1489 url.set_username(username).unwrap();
1490 assert_eq!(
1491 client.get(url).send().await?.status(),
1492 200,
1493 "The password for the username should be pulled from the keyring"
1494 );
1495
1496 let mut url = base_url.clone();
1497 url.set_username(username).unwrap();
1498 url.set_password(Some("invalid")).unwrap();
1499 assert_eq!(
1500 client.get(url).send().await?.status(),
1501 401,
1502 "Password in the URL should take precedence and fail"
1503 );
1504
1505 let mut url = base_url.clone();
1506 url.set_username("other_user").unwrap();
1507 assert!(
1508 matches!(
1509 client.get(url).send().await,
1510 Err(reqwest_middleware::Error::Middleware(_))
1511 ),
1512 "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1513 );
1514
1515 Ok(())
1516 }
1517
1518 #[test(tokio::test)]
1523 async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1524 let username = "user";
1525 let password = "password";
1526 let server = start_test_server(username, password).await;
1527 let base_url = Url::parse(&server.uri())?;
1528
1529 let client = test_client_builder()
1530 .with(
1531 AuthMiddleware::new()
1532 .with_cache(CredentialsCache::new())
1533 .with_keyring(Some(KeyringProvider::dummy([(
1534 base_url.host_str().unwrap(),
1536 username,
1537 password,
1538 )]))),
1539 )
1540 .build();
1541
1542 let mut url = base_url.clone();
1543 url.set_username(username).unwrap();
1544 assert_eq!(
1545 client.get(url).send().await?.status(),
1546 401,
1547 "We should fail because the port is not present in the keyring entry"
1548 );
1549
1550 Ok(())
1551 }
1552
1553 #[test(tokio::test)]
1554 async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1555 let username = "user";
1556 let password = "password";
1557
1558 let server = start_test_server(username, password).await;
1559 let base_url = Url::parse(&server.uri())?;
1560 let cache = CredentialsCache::new();
1561
1562 cache.insert(
1565 DisplaySafeUrl::ref_cast(&base_url),
1566 Arc::new(Authentication::from(Credentials::basic(
1567 Some(username.to_string()),
1568 None,
1569 ))),
1570 );
1571 let client = test_client_builder()
1572 .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1573 KeyringProvider::dummy([(
1574 format!(
1575 "{}:{}",
1576 base_url.host_str().unwrap(),
1577 base_url.port().unwrap()
1578 ),
1579 username,
1580 password,
1581 )]),
1582 )))
1583 .build();
1584
1585 assert_eq!(
1586 client.get(server.uri()).send().await?.status(),
1587 200,
1588 "The username is pulled from the cache, and the password from the keyring"
1589 );
1590
1591 let mut url = base_url.clone();
1592 url.set_username(username).unwrap();
1593 assert_eq!(
1594 client.get(url).send().await?.status(),
1595 200,
1596 "Credentials for the username should be pulled from the keyring"
1597 );
1598
1599 Ok(())
1600 }
1601
1602 #[test(tokio::test)]
1603 async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1604 let username_1 = "user1";
1605 let password_1 = "password1";
1606 let server_1 = start_test_server(username_1, password_1).await;
1607 let base_url_1 = Url::parse(&server_1.uri())?;
1608
1609 let username_2 = "user2";
1610 let password_2 = "password2";
1611 let server_2 = start_test_server(username_2, password_2).await;
1612 let base_url_2 = Url::parse(&server_2.uri())?;
1613
1614 let cache = CredentialsCache::new();
1615 cache.insert(
1617 DisplaySafeUrl::ref_cast(&base_url_1),
1618 Arc::new(Authentication::from(Credentials::basic(
1619 Some(username_1.to_string()),
1620 Some(password_1.to_string()),
1621 ))),
1622 );
1623 cache.insert(
1624 DisplaySafeUrl::ref_cast(&base_url_2),
1625 Arc::new(Authentication::from(Credentials::basic(
1626 Some(username_2.to_string()),
1627 Some(password_2.to_string()),
1628 ))),
1629 );
1630
1631 let client = test_client_builder()
1632 .with(AuthMiddleware::new().with_cache(cache))
1633 .build();
1634
1635 assert_eq!(
1637 client.get(server_1.uri()).send().await?.status(),
1638 200,
1639 "Requests should not require credentials"
1640 );
1641 assert_eq!(
1642 client.get(server_2.uri()).send().await?.status(),
1643 200,
1644 "Requests should not require credentials"
1645 );
1646
1647 assert_eq!(
1648 client
1649 .get(format!("{}/foo", server_1.uri()))
1650 .send()
1651 .await?
1652 .status(),
1653 200,
1654 "Requests can be to different paths in the same realm"
1655 );
1656 assert_eq!(
1657 client
1658 .get(format!("{}/foo", server_2.uri()))
1659 .send()
1660 .await?
1661 .status(),
1662 200,
1663 "Requests can be to different paths in the same realm"
1664 );
1665
1666 Ok(())
1667 }
1668
1669 #[test(tokio::test)]
1670 async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1671 let username_1 = "user1";
1672 let password_1 = "password1";
1673 let server_1 = start_test_server(username_1, password_1).await;
1674 let base_url_1 = Url::parse(&server_1.uri())?;
1675
1676 let username_2 = "user2";
1677 let password_2 = "password2";
1678 let server_2 = start_test_server(username_2, password_2).await;
1679 let base_url_2 = Url::parse(&server_2.uri())?;
1680
1681 let client = test_client_builder()
1682 .with(
1683 AuthMiddleware::new()
1684 .with_cache(CredentialsCache::new())
1685 .with_keyring(Some(KeyringProvider::dummy([
1686 (
1687 format!(
1688 "{}:{}",
1689 base_url_1.host_str().unwrap(),
1690 base_url_1.port().unwrap()
1691 ),
1692 username_1,
1693 password_1,
1694 ),
1695 (
1696 format!(
1697 "{}:{}",
1698 base_url_2.host_str().unwrap(),
1699 base_url_2.port().unwrap()
1700 ),
1701 username_2,
1702 password_2,
1703 ),
1704 ]))),
1705 )
1706 .build();
1707
1708 assert_eq!(
1710 client.get(server_1.uri()).send().await?.status(),
1711 401,
1712 "Requests should require a username"
1713 );
1714 assert_eq!(
1715 client.get(server_2.uri()).send().await?.status(),
1716 401,
1717 "Requests should require a username"
1718 );
1719
1720 let mut url_1 = base_url_1.clone();
1721 url_1.set_username(username_1).unwrap();
1722 assert_eq!(
1723 client.get(url_1.clone()).send().await?.status(),
1724 200,
1725 "Requests with a username should succeed"
1726 );
1727 assert_eq!(
1728 client.get(server_2.uri()).send().await?.status(),
1729 401,
1730 "Credentials should not be re-used for the second server"
1731 );
1732
1733 let mut url_2 = base_url_2.clone();
1734 url_2.set_username(username_2).unwrap();
1735 assert_eq!(
1736 client.get(url_2.clone()).send().await?.status(),
1737 200,
1738 "Requests with a username should succeed"
1739 );
1740
1741 assert_eq!(
1742 client.get(format!("{url_1}/foo")).send().await?.status(),
1743 200,
1744 "Requests can be to different paths in the same realm"
1745 );
1746 assert_eq!(
1747 client.get(format!("{url_2}/foo")).send().await?.status(),
1748 200,
1749 "Requests can be to different paths in the same realm"
1750 );
1751
1752 Ok(())
1753 }
1754
1755 #[test(tokio::test)]
1756 async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1757 let username_1 = "user1";
1758 let password_1 = "password1";
1759 let username_2 = "user2";
1760 let password_2 = "password2";
1761
1762 let server = MockServer::start().await;
1763
1764 Mock::given(method("GET"))
1765 .and(path_regex("/prefix_1.*"))
1766 .and(basic_auth(username_1, password_1))
1767 .respond_with(ResponseTemplate::new(200))
1768 .mount(&server)
1769 .await;
1770
1771 Mock::given(method("GET"))
1772 .and(path_regex("/prefix_2.*"))
1773 .and(basic_auth(username_2, password_2))
1774 .respond_with(ResponseTemplate::new(200))
1775 .mount(&server)
1776 .await;
1777
1778 Mock::given(method("GET"))
1781 .and(path_regex("/prefix_3.*"))
1782 .and(basic_auth(username_1, password_1))
1783 .respond_with(ResponseTemplate::new(401))
1784 .mount(&server)
1785 .await;
1786 Mock::given(method("GET"))
1787 .and(path_regex("/prefix_3.*"))
1788 .and(basic_auth(username_2, password_2))
1789 .respond_with(ResponseTemplate::new(401))
1790 .mount(&server)
1791 .await;
1792 Mock::given(method("GET"))
1793 .and(path_regex("/prefix_3.*"))
1794 .respond_with(ResponseTemplate::new(200))
1795 .mount(&server)
1796 .await;
1797
1798 Mock::given(method("GET"))
1799 .respond_with(ResponseTemplate::new(401))
1800 .mount(&server)
1801 .await;
1802
1803 let base_url = Url::parse(&server.uri())?;
1804 let base_url_1 = base_url.join("prefix_1")?;
1805 let base_url_2 = base_url.join("prefix_2")?;
1806 let base_url_3 = base_url.join("prefix_3")?;
1807
1808 let cache = CredentialsCache::new();
1809
1810 cache.insert(
1812 DisplaySafeUrl::ref_cast(&base_url_1),
1813 Arc::new(Authentication::from(Credentials::basic(
1814 Some(username_1.to_string()),
1815 Some(password_1.to_string()),
1816 ))),
1817 );
1818 cache.insert(
1819 DisplaySafeUrl::ref_cast(&base_url_2),
1820 Arc::new(Authentication::from(Credentials::basic(
1821 Some(username_2.to_string()),
1822 Some(password_2.to_string()),
1823 ))),
1824 );
1825
1826 let client = test_client_builder()
1827 .with(AuthMiddleware::new().with_cache(cache))
1828 .build();
1829
1830 assert_eq!(
1832 client.get(base_url_1.clone()).send().await?.status(),
1833 200,
1834 "Requests should not require credentials"
1835 );
1836 assert_eq!(
1837 client.get(base_url_2.clone()).send().await?.status(),
1838 200,
1839 "Requests should not require credentials"
1840 );
1841 assert_eq!(
1842 client
1843 .get(base_url.join("prefix_1/foo")?)
1844 .send()
1845 .await?
1846 .status(),
1847 200,
1848 "Requests can be to different paths in the same realm"
1849 );
1850 assert_eq!(
1851 client
1852 .get(base_url.join("prefix_2/foo")?)
1853 .send()
1854 .await?
1855 .status(),
1856 200,
1857 "Requests can be to different paths in the same realm"
1858 );
1859 assert_eq!(
1860 client
1861 .get(base_url.join("prefix_1_foo")?)
1862 .send()
1863 .await?
1864 .status(),
1865 401,
1866 "Requests to paths with a matching prefix but different resource segments should fail"
1867 );
1868
1869 assert_eq!(
1870 client.get(base_url_3.clone()).send().await?.status(),
1871 200,
1872 "Requests to the 'public' prefix should not use credentials"
1873 );
1874
1875 Ok(())
1876 }
1877
1878 #[test(tokio::test)]
1879 async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1880 let username_1 = "user1";
1881 let password_1 = "password1";
1882 let username_2 = "user2";
1883 let password_2 = "password2";
1884
1885 let server = MockServer::start().await;
1886
1887 Mock::given(method("GET"))
1888 .and(path_regex("/prefix_1.*"))
1889 .and(basic_auth(username_1, password_1))
1890 .respond_with(ResponseTemplate::new(200))
1891 .mount(&server)
1892 .await;
1893
1894 Mock::given(method("GET"))
1895 .and(path_regex("/prefix_2.*"))
1896 .and(basic_auth(username_2, password_2))
1897 .respond_with(ResponseTemplate::new(200))
1898 .mount(&server)
1899 .await;
1900
1901 Mock::given(method("GET"))
1904 .and(path_regex("/prefix_3.*"))
1905 .and(basic_auth(username_1, password_1))
1906 .respond_with(ResponseTemplate::new(401))
1907 .mount(&server)
1908 .await;
1909 Mock::given(method("GET"))
1910 .and(path_regex("/prefix_3.*"))
1911 .and(basic_auth(username_2, password_2))
1912 .respond_with(ResponseTemplate::new(401))
1913 .mount(&server)
1914 .await;
1915 Mock::given(method("GET"))
1916 .and(path_regex("/prefix_3.*"))
1917 .respond_with(ResponseTemplate::new(200))
1918 .mount(&server)
1919 .await;
1920
1921 Mock::given(method("GET"))
1922 .respond_with(ResponseTemplate::new(401))
1923 .mount(&server)
1924 .await;
1925
1926 let base_url = Url::parse(&server.uri())?;
1927 let base_url_1 = base_url.join("prefix_1")?;
1928 let base_url_2 = base_url.join("prefix_2")?;
1929 let base_url_3 = base_url.join("prefix_3")?;
1930
1931 let client = test_client_builder()
1932 .with(
1933 AuthMiddleware::new()
1934 .with_cache(CredentialsCache::new())
1935 .with_keyring(Some(KeyringProvider::dummy([
1936 (
1937 format!(
1938 "{}:{}",
1939 base_url_1.host_str().unwrap(),
1940 base_url_1.port().unwrap()
1941 ),
1942 username_1,
1943 password_1,
1944 ),
1945 (
1946 format!(
1947 "{}:{}",
1948 base_url_2.host_str().unwrap(),
1949 base_url_2.port().unwrap()
1950 ),
1951 username_2,
1952 password_2,
1953 ),
1954 ]))),
1955 )
1956 .build();
1957
1958 assert_eq!(
1960 client.get(base_url_1.clone()).send().await?.status(),
1961 401,
1962 "Requests should require a username"
1963 );
1964 assert_eq!(
1965 client.get(base_url_2.clone()).send().await?.status(),
1966 401,
1967 "Requests should require a username"
1968 );
1969
1970 let mut url_1 = base_url_1.clone();
1971 url_1.set_username(username_1).unwrap();
1972 assert_eq!(
1973 client.get(url_1.clone()).send().await?.status(),
1974 200,
1975 "Requests with a username should succeed"
1976 );
1977 assert_eq!(
1978 client.get(base_url_2.clone()).send().await?.status(),
1979 401,
1980 "Credentials should not be re-used for the second prefix"
1981 );
1982
1983 let mut url_2 = base_url_2.clone();
1984 url_2.set_username(username_2).unwrap();
1985 assert_eq!(
1986 client.get(url_2.clone()).send().await?.status(),
1987 200,
1988 "Requests with a username should succeed"
1989 );
1990
1991 assert_eq!(
1992 client
1993 .get(base_url.join("prefix_1/foo")?)
1994 .send()
1995 .await?
1996 .status(),
1997 200,
1998 "Requests can be to different paths in the same prefix"
1999 );
2000 assert_eq!(
2001 client
2002 .get(base_url.join("prefix_2/foo")?)
2003 .send()
2004 .await?
2005 .status(),
2006 200,
2007 "Requests can be to different paths in the same prefix"
2008 );
2009 assert_eq!(
2010 client
2011 .get(base_url.join("prefix_1_foo")?)
2012 .send()
2013 .await?
2014 .status(),
2015 401,
2016 "Requests to paths with a matching prefix but different resource segments should fail"
2017 );
2018 assert_eq!(
2019 client.get(base_url_3.clone()).send().await?.status(),
2020 200,
2021 "Requests to the 'public' prefix should not use credentials"
2022 );
2023
2024 Ok(())
2025 }
2026
2027 #[test(tokio::test)]
2031 async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
2032 -> Result<(), Error> {
2033 let username = "user";
2034 let password_1 = "password1";
2035 let password_2 = "password2";
2036
2037 let server = MockServer::start().await;
2038
2039 Mock::given(method("GET"))
2040 .and(path_regex("/prefix_1.*"))
2041 .and(basic_auth(username, password_1))
2042 .respond_with(ResponseTemplate::new(200))
2043 .mount(&server)
2044 .await;
2045
2046 Mock::given(method("GET"))
2047 .and(path_regex("/prefix_2.*"))
2048 .and(basic_auth(username, password_2))
2049 .respond_with(ResponseTemplate::new(200))
2050 .mount(&server)
2051 .await;
2052
2053 Mock::given(method("GET"))
2054 .respond_with(ResponseTemplate::new(401))
2055 .mount(&server)
2056 .await;
2057
2058 let base_url = Url::parse(&server.uri())?;
2059 let base_url_1 = base_url.join("prefix_1")?;
2060 let base_url_2 = base_url.join("prefix_2")?;
2061
2062 let client = test_client_builder()
2063 .with(
2064 AuthMiddleware::new()
2065 .with_cache(CredentialsCache::new())
2066 .with_keyring(Some(KeyringProvider::dummy([
2067 (base_url_1.clone(), username, password_1),
2068 (base_url_2.clone(), username, password_2),
2069 ]))),
2070 )
2071 .build();
2072
2073 assert_eq!(
2075 client.get(base_url_1.clone()).send().await?.status(),
2076 401,
2077 "Requests should require a username"
2078 );
2079 assert_eq!(
2080 client.get(base_url_2.clone()).send().await?.status(),
2081 401,
2082 "Requests should require a username"
2083 );
2084
2085 let mut url_1 = base_url_1.clone();
2086 url_1.set_username(username).unwrap();
2087 assert_eq!(
2088 client.get(url_1.clone()).send().await?.status(),
2089 200,
2090 "The first request with a username will succeed"
2091 );
2092 assert_eq!(
2093 client.get(base_url_2.clone()).send().await?.status(),
2094 401,
2095 "Credentials should not be re-used for the second prefix"
2096 );
2097 assert_eq!(
2098 client
2099 .get(base_url.join("prefix_1/foo")?)
2100 .send()
2101 .await?
2102 .status(),
2103 200,
2104 "Subsequent requests can be to different paths in the same prefix"
2105 );
2106
2107 let mut url_2 = base_url_2.clone();
2108 url_2.set_username(username).unwrap();
2109 assert_eq!(
2110 client.get(url_2.clone()).send().await?.status(),
2111 401, "A request with the same username and realm for a URL that needs a different password will fail"
2113 );
2114 assert_eq!(
2115 client
2116 .get(base_url.join("prefix_2/foo")?)
2117 .send()
2118 .await?
2119 .status(),
2120 401, "Requests to other paths in the failing prefix will also fail"
2122 );
2123
2124 Ok(())
2125 }
2126
2127 #[test(tokio::test)]
2131 async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2132 -> Result<(), Error> {
2133 let username = "user";
2134 let password_1 = "password1";
2135 let password_2 = "password2";
2136
2137 let server = MockServer::start().await;
2138
2139 Mock::given(method("GET"))
2140 .and(path_regex("/prefix_1.*"))
2141 .and(basic_auth(username, password_1))
2142 .respond_with(ResponseTemplate::new(200))
2143 .mount(&server)
2144 .await;
2145
2146 Mock::given(method("GET"))
2147 .and(path_regex("/prefix_2.*"))
2148 .and(basic_auth(username, password_2))
2149 .respond_with(ResponseTemplate::new(200))
2150 .mount(&server)
2151 .await;
2152
2153 Mock::given(method("GET"))
2154 .respond_with(ResponseTemplate::new(401))
2155 .mount(&server)
2156 .await;
2157
2158 let base_url = Url::parse(&server.uri())?;
2159 let base_url_1 = base_url.join("prefix_1")?;
2160 let base_url_2 = base_url.join("prefix_2")?;
2161 let indexes = Indexes::from_indexes(vec![
2162 Index {
2163 url: DisplaySafeUrl::from_url(base_url_1.clone()),
2164 root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2165 auth_policy: AuthPolicy::Auto,
2166 },
2167 Index {
2168 url: DisplaySafeUrl::from_url(base_url_2.clone()),
2169 root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2170 auth_policy: AuthPolicy::Auto,
2171 },
2172 ]);
2173
2174 let client = test_client_builder()
2175 .with(
2176 AuthMiddleware::new()
2177 .with_cache(CredentialsCache::new())
2178 .with_keyring(Some(KeyringProvider::dummy([
2179 (base_url_1.clone(), username, password_1),
2180 (base_url_2.clone(), username, password_2),
2181 ])))
2182 .with_indexes(indexes),
2183 )
2184 .build();
2185
2186 assert_eq!(
2188 client.get(base_url_1.clone()).send().await?.status(),
2189 401,
2190 "Requests should require a username"
2191 );
2192 assert_eq!(
2193 client.get(base_url_2.clone()).send().await?.status(),
2194 401,
2195 "Requests should require a username"
2196 );
2197
2198 let mut url_1 = base_url_1.clone();
2199 url_1.set_username(username).unwrap();
2200 assert_eq!(
2201 client.get(url_1.clone()).send().await?.status(),
2202 200,
2203 "The first request with a username will succeed"
2204 );
2205 assert_eq!(
2206 client.get(base_url_2.clone()).send().await?.status(),
2207 401,
2208 "Credentials should not be re-used for the second prefix"
2209 );
2210 assert_eq!(
2211 client
2212 .get(base_url.join("prefix_1/foo")?)
2213 .send()
2214 .await?
2215 .status(),
2216 200,
2217 "Subsequent requests can be to different paths in the same prefix"
2218 );
2219
2220 let mut url_2 = base_url_2.clone();
2221 url_2.set_username(username).unwrap();
2222 assert_eq!(
2223 client.get(url_2.clone()).send().await?.status(),
2224 200,
2225 "A request with the same username and realm for a URL will use index-specific password"
2226 );
2227 assert_eq!(
2228 client
2229 .get(base_url.join("prefix_2/foo")?)
2230 .send()
2231 .await?
2232 .status(),
2233 200,
2234 "Requests to other paths with that prefix will also succeed"
2235 );
2236
2237 Ok(())
2238 }
2239
2240 #[test(tokio::test)]
2243 async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2244 -> Result<(), Error> {
2245 let username = "user";
2246 let password = "password";
2247
2248 let server = MockServer::start().await;
2249
2250 Mock::given(method("GET"))
2251 .and(basic_auth(username, password))
2252 .respond_with(ResponseTemplate::new(200))
2253 .mount(&server)
2254 .await;
2255
2256 Mock::given(method("GET"))
2257 .and(path_regex("/prefix_1.*"))
2258 .and(basic_auth(username, password))
2259 .respond_with(ResponseTemplate::new(200))
2260 .mount(&server)
2261 .await;
2262
2263 Mock::given(method("GET"))
2264 .respond_with(ResponseTemplate::new(401))
2265 .mount(&server)
2266 .await;
2267
2268 let base_url = Url::parse(&server.uri())?;
2269 let index_url = base_url.join("prefix_1")?;
2270 let indexes = Indexes::from_indexes(vec![Index {
2271 url: DisplaySafeUrl::from_url(index_url.clone()),
2272 root_url: DisplaySafeUrl::from_url(index_url.clone()),
2273 auth_policy: AuthPolicy::Auto,
2274 }]);
2275
2276 let client = test_client_builder()
2277 .with(
2278 AuthMiddleware::new()
2279 .with_cache(CredentialsCache::new())
2280 .with_keyring(Some(KeyringProvider::dummy([(
2281 base_url.clone(),
2282 username,
2283 password,
2284 )])))
2285 .with_indexes(indexes),
2286 )
2287 .build();
2288
2289 assert_eq!(
2291 client.get(index_url.clone()).send().await?.status(),
2292 401,
2293 "Requests should require a username"
2294 );
2295
2296 let mut realm_url = base_url.clone();
2298 realm_url.set_username(username).unwrap();
2299 assert_eq!(
2300 client.get(realm_url.clone()).send().await?.status(),
2301 200,
2302 "The first realm request with a username will succeed"
2303 );
2304
2305 let mut url = index_url.clone();
2306 url.set_username(username).unwrap();
2307 assert_eq!(
2308 client.get(url.clone()).send().await?.status(),
2309 200,
2310 "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2311 );
2312 assert_eq!(
2313 client
2314 .get(base_url.join("prefix_1/foo")?)
2315 .send()
2316 .await?
2317 .status(),
2318 200,
2319 "Requests to other paths with that prefix will also succeed"
2320 );
2321
2322 Ok(())
2323 }
2324
2325 fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2326 let mut url = DisplaySafeUrl::from_url(url.clone());
2327 url.set_password(None).ok();
2328 url.set_username("").ok();
2329 Indexes::from_indexes(vec![Index {
2330 url: url.clone(),
2331 root_url: url.clone(),
2332 auth_policy: policy,
2333 }])
2334 }
2335
2336 #[test(tokio::test)]
2339 async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2340 let username = "user";
2341 let password = "password";
2342
2343 let server = start_test_server(username, password).await;
2344
2345 let base_url = Url::parse(&server.uri())?;
2346
2347 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2348 let client = test_client_builder()
2349 .with(
2350 AuthMiddleware::new()
2351 .with_cache(CredentialsCache::new())
2352 .with_indexes(indexes),
2353 )
2354 .build();
2355
2356 Mock::given(method("GET"))
2357 .and(path_regex("/*"))
2358 .and(basic_auth(username, password))
2359 .respond_with(ResponseTemplate::new(200))
2360 .mount(&server)
2361 .await;
2362
2363 Mock::given(method("GET"))
2364 .respond_with(ResponseTemplate::new(401))
2365 .mount(&server)
2366 .await;
2367
2368 let mut url = base_url.clone();
2369 url.set_username(username).unwrap();
2370 url.set_password(Some(password)).unwrap();
2371 assert_eq!(client.get(url).send().await?.status(), 200);
2372
2373 assert_eq!(
2374 client
2375 .get(format!("{}/foo", server.uri()))
2376 .send()
2377 .await?
2378 .status(),
2379 200,
2380 "Requests can be to different paths with index URL as prefix"
2381 );
2382
2383 let mut url = base_url.clone();
2384 url.set_username(username).unwrap();
2385 url.set_password(Some("invalid")).unwrap();
2386 assert_eq!(
2387 client.get(url).send().await?.status(),
2388 401,
2389 "Incorrect credentials should fail"
2390 );
2391
2392 Ok(())
2393 }
2394
2395 #[test(tokio::test)]
2398 async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2399 let server = MockServer::start().await;
2400
2401 Mock::given(method("GET"))
2402 .and(path_regex("/*"))
2403 .respond_with(ResponseTemplate::new(200))
2404 .mount(&server)
2405 .await;
2406
2407 Mock::given(method("GET"))
2408 .respond_with(ResponseTemplate::new(401))
2409 .mount(&server)
2410 .await;
2411
2412 let base_url = Url::parse(&server.uri())?;
2413
2414 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2415 let client = test_client_builder()
2416 .with(
2417 AuthMiddleware::new()
2418 .with_cache(CredentialsCache::new())
2419 .with_indexes(indexes),
2420 )
2421 .build();
2422
2423 assert!(matches!(
2425 client.get(server.uri()).send().await,
2426 Err(reqwest_middleware::Error::Middleware(_))
2427 ));
2428
2429 Ok(())
2430 }
2431
2432 #[test(tokio::test)]
2435 async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2436 let username = "user";
2437 let password = "password";
2438
2439 let server = start_test_server(username, password).await;
2440 let base_url = Url::parse(&server.uri())?;
2441
2442 Mock::given(method("GET"))
2443 .and(path_regex("/*"))
2444 .and(basic_auth(username, password))
2445 .respond_with(ResponseTemplate::new(200))
2446 .mount(&server)
2447 .await;
2448
2449 Mock::given(method("GET"))
2450 .respond_with(ResponseTemplate::new(401))
2451 .mount(&server)
2452 .await;
2453
2454 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2455 let client = test_client_builder()
2456 .with(
2457 AuthMiddleware::new()
2458 .with_cache(CredentialsCache::new())
2459 .with_indexes(indexes),
2460 )
2461 .build();
2462
2463 let mut url = base_url.clone();
2464 url.set_username(username).unwrap();
2465 url.set_password(Some(password)).unwrap();
2466
2467 assert_eq!(
2468 client
2469 .get(format!("{}/foo", server.uri()))
2470 .send()
2471 .await?
2472 .status(),
2473 401,
2474 "Requests should not be completed if credentials are required"
2475 );
2476
2477 Ok(())
2478 }
2479
2480 #[test(tokio::test)]
2483 async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2484 let server = MockServer::start().await;
2485
2486 Mock::given(method("GET"))
2487 .and(path_regex("/*"))
2488 .respond_with(ResponseTemplate::new(200))
2489 .mount(&server)
2490 .await;
2491
2492 Mock::given(method("GET"))
2493 .respond_with(ResponseTemplate::new(401))
2494 .mount(&server)
2495 .await;
2496
2497 let base_url = Url::parse(&server.uri())?;
2498
2499 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2500 let client = test_client_builder()
2501 .with(
2502 AuthMiddleware::new()
2503 .with_cache(CredentialsCache::new())
2504 .with_indexes(indexes),
2505 )
2506 .build();
2507
2508 assert_eq!(
2509 client.get(server.uri()).send().await?.status(),
2510 200,
2511 "Requests should succeed if unauthenticated requests can succeed"
2512 );
2513
2514 Ok(())
2515 }
2516
2517 #[test]
2518 fn test_tracing_url() {
2519 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2521 assert_eq!(
2522 tracing_url(&req, None),
2523 DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2524 );
2525
2526 let creds = Authentication::from(Credentials::Basic {
2527 username: Username::new(Some(String::from("user"))),
2528 password: None,
2529 });
2530 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2531 assert_eq!(
2532 tracing_url(&req, Some(&creds)),
2533 DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2534 );
2535
2536 let creds = Authentication::from(Credentials::Basic {
2537 username: Username::new(Some(String::from("user"))),
2538 password: Some(Password::new(String::from("password"))),
2539 });
2540 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2541 assert_eq!(
2542 tracing_url(&req, Some(&creds)),
2543 DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2544 .unwrap()
2545 );
2546 }
2547
2548 #[test(tokio::test)]
2549 async fn test_text_store_basic_auth() -> Result<(), Error> {
2550 let username = "user";
2551 let password = "password";
2552
2553 let server = start_test_server(username, password).await;
2554 let base_url = Url::parse(&server.uri())?;
2555
2556 let mut store = TextCredentialStore::default();
2558 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2559 let credentials =
2560 Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2561 store.insert(service.clone(), credentials);
2562
2563 let client = test_client_builder()
2564 .with(
2565 AuthMiddleware::new()
2566 .with_cache(CredentialsCache::new())
2567 .with_text_store(Some(store)),
2568 )
2569 .build();
2570
2571 assert_eq!(
2572 client.get(server.uri()).send().await?.status(),
2573 200,
2574 "Credentials should be pulled from the text store"
2575 );
2576
2577 Ok(())
2578 }
2579
2580 #[test(tokio::test)]
2581 async fn test_text_store_disabled() -> Result<(), Error> {
2582 let username = "user";
2583 let password = "password";
2584 let server = start_test_server(username, password).await;
2585
2586 let client = test_client_builder()
2587 .with(
2588 AuthMiddleware::new()
2589 .with_cache(CredentialsCache::new())
2590 .with_text_store(None), )
2592 .build();
2593
2594 assert_eq!(
2595 client.get(server.uri()).send().await?.status(),
2596 401,
2597 "Credentials should not be found when text store is disabled"
2598 );
2599
2600 Ok(())
2601 }
2602
2603 #[test(tokio::test)]
2604 async fn test_text_store_by_username() -> Result<(), Error> {
2605 let username = "testuser";
2606 let password = "testpass";
2607 let wrong_username = "wronguser";
2608
2609 let server = start_test_server(username, password).await;
2610 let base_url = Url::parse(&server.uri())?;
2611
2612 let mut store = TextCredentialStore::default();
2613 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2614 let credentials =
2615 crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2616 store.insert(service.clone(), credentials);
2617
2618 let client = test_client_builder()
2619 .with(
2620 AuthMiddleware::new()
2621 .with_cache(CredentialsCache::new())
2622 .with_text_store(Some(store)),
2623 )
2624 .build();
2625
2626 let url_with_username = format!(
2628 "{}://{}@{}",
2629 base_url.scheme(),
2630 username,
2631 base_url.host_str().unwrap()
2632 );
2633 let url_with_port = if let Some(port) = base_url.port() {
2634 format!("{}:{}{}", url_with_username, port, base_url.path())
2635 } else {
2636 format!("{}{}", url_with_username, base_url.path())
2637 };
2638
2639 assert_eq!(
2640 client.get(&url_with_port).send().await?.status(),
2641 200,
2642 "Request with matching username should succeed"
2643 );
2644
2645 let url_with_wrong_username = format!(
2647 "{}://{}@{}",
2648 base_url.scheme(),
2649 wrong_username,
2650 base_url.host_str().unwrap()
2651 );
2652 let url_with_port = if let Some(port) = base_url.port() {
2653 format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2654 } else {
2655 format!("{}{}", url_with_wrong_username, base_url.path())
2656 };
2657
2658 assert_eq!(
2659 client.get(&url_with_port).send().await?.status(),
2660 401,
2661 "Request with non-matching username should fail"
2662 );
2663
2664 assert_eq!(
2666 client.get(server.uri()).send().await?.status(),
2667 200,
2668 "Request with no username should succeed"
2669 );
2670
2671 Ok(())
2672 }
2673
2674 fn create_request(url: &str) -> Request {
2675 Request::new(Method::GET, Url::parse(url).unwrap())
2676 }
2677
2678 #[test(tokio::test)]
2683 async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2684 let username = "";
2685 let password = "token";
2686
2687 let server = MockServer::start().await;
2688
2689 Mock::given(method("GET"))
2690 .and(basic_auth(username, password))
2691 .respond_with(ResponseTemplate::new(200))
2692 .mount(&server)
2693 .await;
2694
2695 Mock::given(method("GET"))
2696 .respond_with(ResponseTemplate::new(401))
2697 .mount(&server)
2698 .await;
2699
2700 let client = test_client_builder()
2701 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2702 .build();
2703
2704 let base_url = Url::parse(&server.uri())?;
2705
2706 let mut url = base_url.clone();
2708 url.set_password(Some(password)).unwrap();
2709 assert_eq!(
2710 client.get(url).send().await?.status(),
2711 200,
2712 "URL with empty username but password should authenticate successfully"
2713 );
2714
2715 assert_eq!(
2717 client.get(server.uri()).send().await?.status(),
2718 200,
2719 "Subsequent requests should use cached credentials"
2720 );
2721
2722 assert_eq!(
2723 client
2724 .get(format!("{}/foo", server.uri()))
2725 .send()
2726 .await?
2727 .status(),
2728 200,
2729 "Requests to different paths in the same realm should succeed"
2730 );
2731
2732 Ok(())
2733 }
2734}