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