1use std::borrow::Cow;
2use std::fmt;
3use std::io::Read;
4use std::io::Write;
5use std::str::FromStr;
6
7use base64::prelude::BASE64_STANDARD;
8use base64::read::DecoderReader;
9use base64::write::EncoderWriter;
10use http::Uri;
11use reqsign::aws::DefaultSigner as AwsDefaultSigner;
12use reqsign::azure::DefaultSigner as AzureDefaultSigner;
13use reqsign::google::DefaultSigner as GcsDefaultSigner;
14use reqwest::Request;
15use reqwest::header::{HeaderName, HeaderValue};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use url::Url;
19
20use uv_netrc::Netrc;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24const AZURE_STORAGE_VERSION: &str = "2023-11-03";
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Credentials {
28 Basic {
30 username: Username,
32 password: Option<Password>,
34 },
35 Bearer {
37 token: Token,
39 },
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct Username(Option<String>);
45
46impl Username {
47 pub(crate) fn new(value: Option<String>) -> Self {
51 Self(value.filter(|s| !s.is_empty()))
53 }
54
55 pub(crate) fn none() -> Self {
56 Self::new(None)
57 }
58
59 pub(crate) fn is_none(&self) -> bool {
60 self.0.is_none()
61 }
62
63 pub(crate) fn is_some(&self) -> bool {
64 self.0.is_some()
65 }
66
67 pub(crate) fn as_deref(&self) -> Option<&str> {
68 self.0.as_deref()
69 }
70}
71
72impl From<String> for Username {
73 fn from(value: String) -> Self {
74 Self::new(Some(value))
75 }
76}
77
78impl From<Option<String>> for Username {
79 fn from(value: Option<String>) -> Self {
80 Self::new(value)
81 }
82}
83
84#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct Password(String);
87
88impl Password {
89 pub fn new(password: String) -> Self {
90 Self(password)
91 }
92
93 pub(crate) fn as_str(&self) -> &str {
95 self.0.as_str()
96 }
97}
98
99impl fmt::Debug for Password {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "****")
102 }
103}
104
105#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
106#[serde(transparent)]
107pub struct Token(Vec<u8>);
108
109impl Token {
110 pub(crate) fn new(token: Vec<u8>) -> Self {
111 Self(token)
112 }
113
114 pub(crate) fn as_slice(&self) -> &[u8] {
116 self.0.as_slice()
117 }
118
119 pub(crate) fn into_bytes(self) -> Vec<u8> {
121 self.0
122 }
123
124 pub(crate) fn is_empty(&self) -> bool {
126 self.0.is_empty()
127 }
128}
129
130impl fmt::Debug for Token {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "****")
133 }
134}
135impl Credentials {
136 #[allow(dead_code)]
138 pub fn basic(username: Option<String>, password: Option<String>) -> Self {
139 Self::Basic {
140 username: Username::new(username),
141 password: password.map(Password),
142 }
143 }
144
145 #[allow(dead_code)]
147 pub fn bearer(token: Vec<u8>) -> Self {
148 Self::Bearer {
149 token: Token::new(token),
150 }
151 }
152
153 pub fn username(&self) -> Option<&str> {
154 match self {
155 Self::Basic { username, .. } => username.as_deref(),
156 Self::Bearer { .. } => None,
157 }
158 }
159
160 pub(crate) fn to_username(&self) -> Username {
161 match self {
162 Self::Basic { username, .. } => username.clone(),
163 Self::Bearer { .. } => Username::none(),
164 }
165 }
166
167 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
168 match self {
169 Self::Basic { username, .. } => Cow::Borrowed(username),
170 Self::Bearer { .. } => Cow::Owned(Username::none()),
171 }
172 }
173
174 pub fn password(&self) -> Option<&str> {
175 match self {
176 Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
177 Self::Bearer { .. } => None,
178 }
179 }
180
181 pub(crate) fn is_authenticated(&self) -> bool {
182 match self {
183 Self::Basic {
184 username: _,
185 password,
186 } => password.is_some(),
187 Self::Bearer { token } => !token.is_empty(),
188 }
189 }
190
191 pub(crate) fn is_empty(&self) -> bool {
192 match self {
193 Self::Basic { username, password } => username.is_none() && password.is_none(),
194 Self::Bearer { token } => token.is_empty(),
195 }
196 }
197
198 pub(crate) fn from_netrc(
202 netrc: &Netrc,
203 url: &DisplaySafeUrl,
204 username: Option<&str>,
205 ) -> Option<Self> {
206 let host = url.host_str()?;
207 let entry = netrc
208 .hosts
209 .get(host)
210 .or_else(|| netrc.hosts.get("default"))?;
211
212 if username.is_some_and(|username| username != entry.login) {
214 return None;
215 }
216
217 Some(Self::Basic {
218 username: Username::new(Some(entry.login.clone())),
219 password: Some(Password(entry.password.clone())),
220 })
221 }
222
223 pub fn from_url(url: &Url) -> Option<Self> {
227 if url.username().is_empty() && url.password().is_none() {
228 return None;
229 }
230 Some(Self::Basic {
231 username: if url.username().is_empty() {
234 None
235 } else {
236 Some(
237 percent_encoding::percent_decode_str(url.username())
238 .decode_utf8()
239 .expect("An encoded username should always decode")
240 .into_owned(),
241 )
242 }
243 .into(),
244 password: url.password().map(|password| {
245 Password(
246 percent_encoding::percent_decode_str(password)
247 .decode_utf8()
248 .expect("An encoded password should always decode")
249 .into_owned(),
250 )
251 }),
252 })
253 }
254
255 pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
260 let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
261 let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
262 if username.is_none() && password.is_none() {
263 None
264 } else {
265 Some(Self::basic(username, password))
266 }
267 }
268
269 pub(crate) fn from_request(request: &Request) -> Option<Self> {
273 Self::from_url(request.url()).or(
275 request
277 .headers()
278 .get(reqwest::header::AUTHORIZATION)
279 .map(Self::from_header_value)?,
280 )
281 }
282
283 fn from_header_value(header: &HeaderValue) -> Option<Self> {
292 if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
294 let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
295 let mut buf = String::new();
296 decoder
297 .read_to_string(&mut buf)
298 .expect("HTTP Basic Authentication should be base64 encoded");
299 let (username, password) = buf
300 .split_once(':')
301 .expect("HTTP Basic Authentication should include a `:` separator");
302 let username = if username.is_empty() {
303 None
304 } else {
305 Some(username.to_string())
306 };
307 let password = if password.is_empty() {
308 None
309 } else {
310 Some(password.to_string())
311 };
312 return Some(Self::Basic {
313 username: Username::new(username),
314 password: password.map(Password),
315 });
316 }
317
318 if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
320 return Some(Self::Bearer {
321 token: Token::new(token.to_vec()),
322 });
323 }
324
325 None
326 }
327
328 pub fn to_header_value(&self) -> HeaderValue {
332 match self {
333 Self::Basic { .. } => {
334 let mut buf = b"Basic ".to_vec();
336 {
337 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
338 write!(encoder, "{}:", self.username().unwrap_or_default())
339 .expect("Write to base64 encoder should succeed");
340 if let Some(password) = self.password() {
341 write!(encoder, "{password}")
342 .expect("Write to base64 encoder should succeed");
343 }
344 }
345 let mut header =
346 HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
347 header.set_sensitive(true);
348 header
349 }
350 Self::Bearer { token } => {
351 let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
352 .expect("Bearer token is always valid HeaderValue");
353 header.set_sensitive(true);
354 header
355 }
356 }
357 }
358
359 #[must_use]
363 pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
364 if let Some(username) = self.username() {
365 let _ = url.set_username(username);
366 }
367 if let Some(password) = self.password() {
368 let _ = url.set_password(Some(password));
369 }
370 url
371 }
372
373 #[must_use]
377 pub fn authenticate(&self, mut request: Request) -> Request {
378 request
379 .headers_mut()
380 .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
381 request
382 }
383}
384
385#[derive(Clone, Debug)]
386pub(crate) enum Authentication {
387 Credentials(Credentials),
389
390 AwsSigner(AwsDefaultSigner),
392
393 GcsSigner(GcsDefaultSigner),
395
396 AzureSigner(AzureDefaultSigner),
398}
399
400#[derive(Debug, Error)]
401pub(crate) enum AuthenticationError {
402 #[error("Failed to convert request URL to URI")]
403 InvalidUri(#[from] http::uri::InvalidUri),
404
405 #[error("Failed to build request for {provider} signing")]
406 BuildRequest {
407 provider: &'static str,
408 #[source]
409 source: http::Error,
410 },
411
412 #[error("Failed to sign request with {provider} credentials")]
413 Sign {
414 provider: &'static str,
415 #[source]
416 source: reqsign::Error,
417 },
418}
419
420impl PartialEq for Authentication {
421 fn eq(&self, other: &Self) -> bool {
422 match (self, other) {
423 (Self::Credentials(a), Self::Credentials(b)) => a == b,
424 (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
425 (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
426 (Self::AzureSigner(..), Self::AzureSigner(..)) => true,
427 _ => false,
428 }
429 }
430}
431
432impl Eq for Authentication {}
433
434impl From<Credentials> for Authentication {
435 fn from(credentials: Credentials) -> Self {
436 Self::Credentials(credentials)
437 }
438}
439
440impl From<AwsDefaultSigner> for Authentication {
441 fn from(signer: AwsDefaultSigner) -> Self {
442 Self::AwsSigner(signer)
443 }
444}
445
446impl From<GcsDefaultSigner> for Authentication {
447 fn from(signer: GcsDefaultSigner) -> Self {
448 Self::GcsSigner(signer)
449 }
450}
451
452impl From<AzureDefaultSigner> for Authentication {
453 fn from(signer: AzureDefaultSigner) -> Self {
454 Self::AzureSigner(signer)
455 }
456}
457
458impl Authentication {
459 pub(crate) fn password(&self) -> Option<&str> {
461 match self {
462 Self::Credentials(credentials) => credentials.password(),
463 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
464 }
465 }
466
467 pub(crate) fn username(&self) -> Option<&str> {
469 match self {
470 Self::Credentials(credentials) => credentials.username(),
471 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
472 }
473 }
474
475 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
477 match self {
478 Self::Credentials(credentials) => credentials.as_username(),
479 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => {
480 Cow::Owned(Username::none())
481 }
482 }
483 }
484
485 pub(crate) fn to_username(&self) -> Username {
487 match self {
488 Self::Credentials(credentials) => credentials.to_username(),
489 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(),
490 }
491 }
492
493 pub(crate) fn is_authenticated(&self) -> bool {
495 match self {
496 Self::Credentials(credentials) => credentials.is_authenticated(),
497 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true,
498 }
499 }
500
501 pub(crate) fn is_empty(&self) -> bool {
503 match self {
504 Self::Credentials(credentials) => credentials.is_empty(),
505 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false,
506 }
507 }
508
509 pub(crate) async fn authenticate(
513 &self,
514 mut request: Request,
515 ) -> Result<Request, AuthenticationError> {
516 match self {
517 Self::Credentials(credentials) => Ok(credentials.authenticate(request)),
518 Self::AwsSigner(signer) => {
519 let uri = Uri::from_str(request.url().as_str())?;
521 let mut http_req = http::Request::builder()
522 .method(request.method().clone())
523 .uri(uri)
524 .body(())
525 .map_err(|source| AuthenticationError::BuildRequest {
526 provider: "AWS",
527 source,
528 })?;
529 *http_req.headers_mut() = request.headers().clone();
530
531 let (mut parts, ()) = http_req.into_parts();
533 signer.sign(&mut parts, None).await.map_err(|source| {
534 AuthenticationError::Sign {
535 provider: "AWS",
536 source,
537 }
538 })?;
539
540 request.headers_mut().extend(parts.headers);
542
543 if let Some(path_and_query) = parts.uri.path_and_query() {
545 request.url_mut().set_path(path_and_query.path());
546 request.url_mut().set_query(path_and_query.query());
547 }
548 Ok(request)
549 }
550 Self::GcsSigner(signer) => {
551 let uri = Uri::from_str(request.url().as_str())?;
553 let mut http_req = http::Request::builder()
554 .method(request.method().clone())
555 .uri(uri)
556 .body(())
557 .map_err(|source| AuthenticationError::BuildRequest {
558 provider: "GCS",
559 source,
560 })?;
561 *http_req.headers_mut() = request.headers().clone();
562
563 let (mut parts, ()) = http_req.into_parts();
565 signer.sign(&mut parts, None).await.map_err(|source| {
566 AuthenticationError::Sign {
567 provider: "GCS",
568 source,
569 }
570 })?;
571
572 request.headers_mut().extend(parts.headers);
574
575 if let Some(path_and_query) = parts.uri.path_and_query() {
577 request.url_mut().set_path(path_and_query.path());
578 request.url_mut().set_query(path_and_query.query());
579 }
580 Ok(request)
581 }
582 Self::AzureSigner(signer) => {
583 let uri = Uri::from_str(request.url().as_str())?;
585 let mut http_req = http::Request::builder()
586 .method(request.method().clone())
587 .uri(uri)
588 .body(())
589 .map_err(|source| AuthenticationError::BuildRequest {
590 provider: "Azure",
591 source,
592 })?;
593 *http_req.headers_mut() = request.headers().clone();
594 http_req
595 .headers_mut()
596 .entry(HeaderName::from_static("x-ms-version"))
597 .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION));
598
599 let (mut parts, ()) = http_req.into_parts();
601 signer.sign(&mut parts, None).await.map_err(|source| {
602 AuthenticationError::Sign {
603 provider: "Azure",
604 source,
605 }
606 })?;
607
608 request.headers_mut().extend(parts.headers);
610
611 if let Some(path_and_query) = parts.uri.path_and_query() {
613 request.url_mut().set_path(path_and_query.path());
614 request.url_mut().set_query(path_and_query.query());
615 }
616 Ok(request)
617 }
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use insta::assert_debug_snapshot;
625 use reqsign::aws::Credential as AwsCredential;
626 use reqsign::azure::Credential as AzureCredential;
627 use reqsign::{Context, ProvideCredential};
628
629 use super::*;
630
631 #[derive(Debug)]
632 struct EmptyAwsCredentialProvider;
633
634 impl ProvideCredential for EmptyAwsCredentialProvider {
635 type Credential = AwsCredential;
636
637 async fn provide_credential(
638 &self,
639 _ctx: &Context,
640 ) -> reqsign::Result<Option<Self::Credential>> {
641 Ok(None)
642 }
643 }
644
645 #[derive(Debug)]
646 struct EmptyAzureCredentialProvider;
647
648 impl ProvideCredential for EmptyAzureCredentialProvider {
649 type Credential = AzureCredential;
650
651 async fn provide_credential(
652 &self,
653 _ctx: &Context,
654 ) -> reqsign::Result<Option<Self::Credential>> {
655 Ok(None)
656 }
657 }
658
659 #[test]
660 fn from_url_no_credentials() {
661 let url = &Url::parse("https://example.com/simple/first/").unwrap();
662 assert_eq!(Credentials::from_url(url), None);
663 }
664
665 #[test]
666 fn from_url_username_and_password() {
667 let url = &Url::parse("https://example.com/simple/first/").unwrap();
668 let mut auth_url = url.clone();
669 auth_url.set_username("user").unwrap();
670 auth_url.set_password(Some("password")).unwrap();
671 let credentials = Credentials::from_url(&auth_url).unwrap();
672 assert_eq!(credentials.username(), Some("user"));
673 assert_eq!(credentials.password(), Some("password"));
674 }
675
676 #[test]
677 fn from_url_no_username() {
678 let url = &Url::parse("https://example.com/simple/first/").unwrap();
679 let mut auth_url = url.clone();
680 auth_url.set_password(Some("password")).unwrap();
681 let credentials = Credentials::from_url(&auth_url).unwrap();
682 assert_eq!(credentials.username(), None);
683 assert_eq!(credentials.password(), Some("password"));
684 }
685
686 #[test]
691 fn from_url_empty_username_with_password() {
692 let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
694 let credentials = Credentials::from_url(&url).unwrap();
695 assert_eq!(credentials.username(), None);
696 assert_eq!(credentials.password(), Some("token"));
697 assert!(
698 credentials.is_authenticated(),
699 "URL with empty username but password should be considered authenticated"
700 );
701 }
702
703 #[test]
704 fn from_url_no_password() {
705 let url = &Url::parse("https://example.com/simple/first/").unwrap();
706 let mut auth_url = url.clone();
707 auth_url.set_username("user").unwrap();
708 let credentials = Credentials::from_url(&auth_url).unwrap();
709 assert_eq!(credentials.username(), Some("user"));
710 assert_eq!(credentials.password(), None);
711 }
712
713 #[test]
714 fn authenticated_request_from_url() {
715 let url = Url::parse("https://example.com/simple/first/").unwrap();
716 let mut auth_url = url.clone();
717 auth_url.set_username("user").unwrap();
718 auth_url.set_password(Some("password")).unwrap();
719 let credentials = Credentials::from_url(&auth_url).unwrap();
720
721 let mut request = Request::new(reqwest::Method::GET, url);
722 request = credentials.authenticate(request);
723
724 let mut header = request
725 .headers()
726 .get(reqwest::header::AUTHORIZATION)
727 .expect("Authorization header should be set")
728 .clone();
729 header.set_sensitive(false);
730
731 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
732 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
733 }
734
735 #[test]
736 fn authenticated_request_from_url_with_percent_encoded_user() {
737 let url = Url::parse("https://example.com/simple/first/").unwrap();
738 let mut auth_url = url.clone();
739 auth_url.set_username("user@domain").unwrap();
740 auth_url.set_password(Some("password")).unwrap();
741 let credentials = Credentials::from_url(&auth_url).unwrap();
742
743 let mut request = Request::new(reqwest::Method::GET, url);
744 request = credentials.authenticate(request);
745
746 let mut header = request
747 .headers()
748 .get(reqwest::header::AUTHORIZATION)
749 .expect("Authorization header should be set")
750 .clone();
751 header.set_sensitive(false);
752
753 assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
754 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
755 }
756
757 #[test]
758 fn authenticated_request_from_url_with_percent_encoded_password() {
759 let url = Url::parse("https://example.com/simple/first/").unwrap();
760 let mut auth_url = url.clone();
761 auth_url.set_username("user").unwrap();
762 auth_url.set_password(Some("password==")).unwrap();
763 let credentials = Credentials::from_url(&auth_url).unwrap();
764
765 let mut request = Request::new(reqwest::Method::GET, url);
766 request = credentials.authenticate(request);
767
768 let mut header = request
769 .headers()
770 .get(reqwest::header::AUTHORIZATION)
771 .expect("Authorization header should be set")
772 .clone();
773 header.set_sensitive(false);
774
775 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
776 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
777 }
778
779 #[tokio::test]
780 async fn authenticated_request_with_azure_signer() {
781 let signer = reqsign::azure::default_signer().with_credential_provider(
782 reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
783 );
784 let authentication = Authentication::from(signer);
785
786 let request = Request::new(
787 reqwest::Method::GET,
788 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
789 );
790 let request = authentication.authenticate(request).await.unwrap();
791
792 let authorization = request
793 .headers()
794 .get(reqwest::header::AUTHORIZATION)
795 .expect("Authorization header should be set");
796 assert_eq!(authorization.to_str().unwrap(), "Bearer token");
797 assert!(request.headers().contains_key("x-ms-date"));
798 assert_eq!(
799 request
800 .headers()
801 .get("x-ms-version")
802 .expect("x-ms-version header should be set")
803 .to_str()
804 .unwrap(),
805 AZURE_STORAGE_VERSION
806 );
807 }
808
809 #[tokio::test]
810 async fn authenticated_request_with_aws_signer_missing_credentials() {
811 let signer = reqsign::aws::default_signer("s3", "us-east-1")
812 .with_credential_provider(EmptyAwsCredentialProvider);
813 let authentication = Authentication::from(signer);
814
815 let request = Request::new(
816 reqwest::Method::GET,
817 Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
818 );
819 let err = authentication.authenticate(request).await.unwrap_err();
820
821 insta::assert_snapshot!(
822 err.to_string(),
823 @"Failed to sign request with AWS credentials"
824 );
825 }
826
827 #[tokio::test]
828 async fn authenticated_request_with_azure_signer_missing_credentials() {
829 let signer =
830 reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
831 let authentication = Authentication::from(signer);
832
833 let request = Request::new(
834 reqwest::Method::GET,
835 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
836 );
837 let err = authentication.authenticate(request).await.unwrap_err();
838
839 insta::assert_snapshot!(
840 err.to_string(),
841 @"Failed to sign request with Azure credentials"
842 );
843 }
844
845 #[test]
847 fn test_password_redaction() {
848 let credentials =
849 Credentials::basic(Some(String::from("user")), Some(String::from("password")));
850 insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
851 }
852
853 #[test]
855 fn test_bearer_token_redaction() {
856 let token = "super_secret_token";
857 let credentials = Credentials::bearer(token.into());
858 insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
859 }
860}