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