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