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