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