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