1use std::borrow::Cow;
8
9#[cfg(feature = "zeroize")]
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12#[derive(Clone)]
17#[non_exhaustive]
18pub enum Credentials {
19 SqlServer {
21 username: Cow<'static, str>,
23 password: Cow<'static, str>,
25 },
26
27 AzureAccessToken {
29 token: Cow<'static, str>,
31 },
32
33 #[cfg(feature = "azure-identity")]
35 AzureManagedIdentity {
36 client_id: Option<Cow<'static, str>>,
38 },
39
40 #[cfg(feature = "azure-identity")]
42 AzureServicePrincipal {
43 tenant_id: Cow<'static, str>,
45 client_id: Cow<'static, str>,
47 client_secret: Cow<'static, str>,
49 },
50
51 #[cfg(feature = "azure-identity")]
54 AzureDefault,
55
56 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
58 Integrated,
59
60 #[cfg(feature = "cert-auth")]
68 Certificate {
69 tenant_id: Cow<'static, str>,
71 client_id: Cow<'static, str>,
73 cert_path: Cow<'static, str>,
76 password: Option<Cow<'static, str>>,
78 },
79}
80
81impl Credentials {
82 pub fn sql_server(
84 username: impl Into<Cow<'static, str>>,
85 password: impl Into<Cow<'static, str>>,
86 ) -> Self {
87 Self::SqlServer {
88 username: username.into(),
89 password: password.into(),
90 }
91 }
92
93 pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
95 Self::AzureAccessToken {
96 token: token.into(),
97 }
98 }
99
100 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
104 #[must_use]
105 pub fn integrated() -> Self {
106 Self::Integrated
107 }
108
109 #[cfg(feature = "azure-identity")]
115 #[must_use]
116 pub fn azure_default() -> Self {
117 Self::AzureDefault
118 }
119
120 #[cfg(feature = "cert-auth")]
128 pub fn certificate(
129 tenant_id: impl Into<Cow<'static, str>>,
130 client_id: impl Into<Cow<'static, str>>,
131 cert_path: impl Into<Cow<'static, str>>,
132 password: Option<Cow<'static, str>>,
133 ) -> Self {
134 Self::Certificate {
135 tenant_id: tenant_id.into(),
136 client_id: client_id.into(),
137 cert_path: cert_path.into(),
138 password,
139 }
140 }
141
142 #[must_use]
144 pub fn is_sql_auth(&self) -> bool {
145 matches!(self, Self::SqlServer { .. })
146 }
147
148 #[must_use]
150 pub fn is_azure_ad(&self) -> bool {
151 #[allow(clippy::match_like_matches_macro)]
152 match self {
153 Self::AzureAccessToken { .. } => true,
154 #[cfg(feature = "azure-identity")]
155 Self::AzureManagedIdentity { .. }
156 | Self::AzureServicePrincipal { .. }
157 | Self::AzureDefault => true,
158 _ => false,
159 }
160 }
161
162 #[must_use]
164 pub fn method_name(&self) -> &'static str {
165 match self {
166 Self::SqlServer { .. } => "SQL Server Authentication",
167 Self::AzureAccessToken { .. } => "Azure AD Access Token",
168 #[cfg(feature = "azure-identity")]
169 Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
170 #[cfg(feature = "azure-identity")]
171 Self::AzureServicePrincipal { .. } => "Azure Service Principal",
172 #[cfg(feature = "azure-identity")]
173 Self::AzureDefault => "Azure Default Credential Chain",
174 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
175 Self::Integrated => "Integrated Authentication",
176 #[cfg(feature = "cert-auth")]
177 Self::Certificate { .. } => "Certificate Authentication",
178 }
179 }
180}
181
182impl std::fmt::Debug for Credentials {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 match self {
186 Self::SqlServer { username, .. } => f
187 .debug_struct("SqlServer")
188 .field("username", username)
189 .field("password", &"[REDACTED]")
190 .finish(),
191 Self::AzureAccessToken { .. } => f
192 .debug_struct("AzureAccessToken")
193 .field("token", &"[REDACTED]")
194 .finish(),
195 #[cfg(feature = "azure-identity")]
196 Self::AzureManagedIdentity { client_id } => f
197 .debug_struct("AzureManagedIdentity")
198 .field("client_id", client_id)
199 .finish(),
200 #[cfg(feature = "azure-identity")]
201 Self::AzureServicePrincipal {
202 tenant_id,
203 client_id,
204 ..
205 } => f
206 .debug_struct("AzureServicePrincipal")
207 .field("tenant_id", tenant_id)
208 .field("client_id", client_id)
209 .field("client_secret", &"[REDACTED]")
210 .finish(),
211 #[cfg(feature = "azure-identity")]
212 Self::AzureDefault => f.debug_struct("AzureDefault").finish(),
213 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
214 Self::Integrated => f.debug_struct("Integrated").finish(),
215 #[cfg(feature = "cert-auth")]
216 Self::Certificate {
217 tenant_id,
218 client_id,
219 cert_path,
220 ..
221 } => f
222 .debug_struct("Certificate")
223 .field("tenant_id", tenant_id)
224 .field("client_id", client_id)
225 .field("cert_path", cert_path)
226 .field("password", &"[REDACTED]")
227 .finish(),
228 }
229 }
230}
231
232#[cfg(feature = "zeroize")]
242#[derive(Clone, Zeroize, ZeroizeOnDrop)]
243pub struct SecretString(String);
244
245#[cfg(feature = "zeroize")]
246impl SecretString {
247 pub fn new(value: impl Into<String>) -> Self {
249 Self(value.into())
250 }
251
252 #[must_use]
259 pub fn expose_secret(&self) -> &str {
260 &self.0
261 }
262}
263
264#[cfg(feature = "zeroize")]
265impl std::fmt::Debug for SecretString {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(f, "[REDACTED]")
268 }
269}
270
271#[cfg(feature = "zeroize")]
272impl From<String> for SecretString {
273 fn from(s: String) -> Self {
274 Self::new(s)
275 }
276}
277
278#[cfg(feature = "zeroize")]
279impl From<&str> for SecretString {
280 fn from(s: &str) -> Self {
281 Self::new(s)
282 }
283}
284
285#[cfg(feature = "zeroize")]
300#[derive(Clone, Zeroize, ZeroizeOnDrop)]
301pub struct SecureCredentials {
302 kind: SecureCredentialKind,
303}
304
305#[cfg(feature = "zeroize")]
306#[derive(Clone, Zeroize, ZeroizeOnDrop)]
307enum SecureCredentialKind {
308 SqlServer {
309 username: String,
310 password: SecretString,
311 },
312 AzureAccessToken {
313 token: SecretString,
314 },
315 #[cfg(feature = "azure-identity")]
316 AzureManagedIdentity {
317 client_id: Option<String>,
318 },
319 #[cfg(feature = "azure-identity")]
320 AzureServicePrincipal {
321 tenant_id: String,
322 client_id: String,
323 client_secret: SecretString,
324 },
325 #[cfg(feature = "azure-identity")]
326 AzureDefault,
327 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
328 Integrated,
329 #[cfg(feature = "cert-auth")]
330 Certificate {
331 tenant_id: String,
332 client_id: String,
333 cert_path: String,
334 password: Option<SecretString>,
335 },
336}
337
338#[cfg(feature = "zeroize")]
339impl SecureCredentials {
340 pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
342 Self {
343 kind: SecureCredentialKind::SqlServer {
344 username: username.into(),
345 password: SecretString::new(password),
346 },
347 }
348 }
349
350 pub fn azure_token(token: impl Into<String>) -> Self {
352 Self {
353 kind: SecureCredentialKind::AzureAccessToken {
354 token: SecretString::new(token),
355 },
356 }
357 }
358
359 #[must_use]
361 pub fn is_sql_auth(&self) -> bool {
362 matches!(self.kind, SecureCredentialKind::SqlServer { .. })
363 }
364
365 #[must_use]
367 pub fn is_azure_ad(&self) -> bool {
368 #[allow(clippy::match_like_matches_macro)]
369 match &self.kind {
370 SecureCredentialKind::AzureAccessToken { .. } => true,
371 #[cfg(feature = "azure-identity")]
372 SecureCredentialKind::AzureManagedIdentity { .. }
373 | SecureCredentialKind::AzureServicePrincipal { .. } => true,
374 _ => false,
375 }
376 }
377
378 #[must_use]
380 pub fn method_name(&self) -> &'static str {
381 match &self.kind {
382 SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
383 SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
384 #[cfg(feature = "azure-identity")]
385 SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
386 #[cfg(feature = "azure-identity")]
387 SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
388 #[cfg(feature = "azure-identity")]
389 SecureCredentialKind::AzureDefault => "Azure Default Credential Chain",
390 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
391 SecureCredentialKind::Integrated => "Integrated Authentication",
392 #[cfg(feature = "cert-auth")]
393 SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
394 }
395 }
396
397 #[must_use]
401 pub fn username(&self) -> Option<&str> {
402 match &self.kind {
403 SecureCredentialKind::SqlServer { username, .. } => Some(username),
404 _ => None,
405 }
406 }
407
408 #[must_use]
417 pub fn password(&self) -> Option<&str> {
418 match &self.kind {
419 SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
420 _ => None,
421 }
422 }
423
424 #[must_use]
433 pub fn token(&self) -> Option<&str> {
434 match &self.kind {
435 SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
436 _ => None,
437 }
438 }
439}
440
441#[cfg(feature = "zeroize")]
442impl std::fmt::Debug for SecureCredentials {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 match &self.kind {
445 SecureCredentialKind::SqlServer { username, .. } => f
446 .debug_struct("SecureCredentials::SqlServer")
447 .field("username", username)
448 .field("password", &"[REDACTED]")
449 .finish(),
450 SecureCredentialKind::AzureAccessToken { .. } => f
451 .debug_struct("SecureCredentials::AzureAccessToken")
452 .field("token", &"[REDACTED]")
453 .finish(),
454 #[cfg(feature = "azure-identity")]
455 SecureCredentialKind::AzureManagedIdentity { client_id } => f
456 .debug_struct("SecureCredentials::AzureManagedIdentity")
457 .field("client_id", client_id)
458 .finish(),
459 #[cfg(feature = "azure-identity")]
460 SecureCredentialKind::AzureServicePrincipal {
461 tenant_id,
462 client_id,
463 ..
464 } => f
465 .debug_struct("SecureCredentials::AzureServicePrincipal")
466 .field("tenant_id", tenant_id)
467 .field("client_id", client_id)
468 .field("client_secret", &"[REDACTED]")
469 .finish(),
470 #[cfg(feature = "azure-identity")]
471 SecureCredentialKind::AzureDefault => {
472 f.debug_struct("SecureCredentials::AzureDefault").finish()
473 }
474 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
475 SecureCredentialKind::Integrated => {
476 f.debug_struct("SecureCredentials::Integrated").finish()
477 }
478 #[cfg(feature = "cert-auth")]
479 SecureCredentialKind::Certificate {
480 tenant_id,
481 client_id,
482 cert_path,
483 ..
484 } => f
485 .debug_struct("SecureCredentials::Certificate")
486 .field("tenant_id", tenant_id)
487 .field("client_id", client_id)
488 .field("cert_path", cert_path)
489 .field("password", &"[REDACTED]")
490 .finish(),
491 }
492 }
493}
494
495#[cfg(feature = "zeroize")]
497impl From<Credentials> for SecureCredentials {
498 fn from(creds: Credentials) -> Self {
499 match creds {
500 Credentials::SqlServer { username, password } => {
501 SecureCredentials::sql_server(username.into_owned(), password.into_owned())
502 }
503 Credentials::AzureAccessToken { token } => {
504 SecureCredentials::azure_token(token.into_owned())
505 }
506 #[cfg(feature = "azure-identity")]
507 Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
508 kind: SecureCredentialKind::AzureManagedIdentity {
509 client_id: client_id.map(|c| c.into_owned()),
510 },
511 },
512 #[cfg(feature = "azure-identity")]
513 Credentials::AzureServicePrincipal {
514 tenant_id,
515 client_id,
516 client_secret,
517 } => SecureCredentials {
518 kind: SecureCredentialKind::AzureServicePrincipal {
519 tenant_id: tenant_id.into_owned(),
520 client_id: client_id.into_owned(),
521 client_secret: SecretString::new(client_secret.into_owned()),
522 },
523 },
524 #[cfg(feature = "azure-identity")]
525 Credentials::AzureDefault => SecureCredentials {
526 kind: SecureCredentialKind::AzureDefault,
527 },
528 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
529 Credentials::Integrated => SecureCredentials {
530 kind: SecureCredentialKind::Integrated,
531 },
532 #[cfg(feature = "cert-auth")]
533 Credentials::Certificate {
534 tenant_id,
535 client_id,
536 cert_path,
537 password,
538 } => SecureCredentials {
539 kind: SecureCredentialKind::Certificate {
540 tenant_id: tenant_id.into_owned(),
541 client_id: client_id.into_owned(),
542 cert_path: cert_path.into_owned(),
543 password: password.map(|p| SecretString::new(p.into_owned())),
544 },
545 },
546 }
547 }
548}
549
550#[cfg(test)]
551#[allow(clippy::panic)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn test_credentials_sql_server() {
557 let creds = Credentials::sql_server("user", "password");
558 assert!(creds.is_sql_auth());
559 assert!(!creds.is_azure_ad());
560 match creds {
561 Credentials::SqlServer { username, password } => {
562 assert_eq!(username.as_ref(), "user");
563 assert_eq!(password.as_ref(), "password");
564 }
565 _ => panic!("Expected SqlServer variant"),
566 }
567 }
568
569 #[test]
570 fn test_credentials_azure_token() {
571 let creds = Credentials::azure_token("my-token");
572 assert!(!creds.is_sql_auth());
573 assert!(creds.is_azure_ad());
574 match creds {
575 Credentials::AzureAccessToken { token } => {
576 assert_eq!(token.as_ref(), "my-token");
577 }
578 _ => panic!("Expected AzureAccessToken variant"),
579 }
580 }
581
582 #[test]
583 fn test_credentials_debug_redacts_password() {
584 let creds = Credentials::sql_server("user", "supersecret");
585 let debug = format!("{creds:?}");
586 assert!(debug.contains("user"));
587 assert!(!debug.contains("supersecret"));
588 assert!(debug.contains("REDACTED"));
589 }
590
591 #[test]
592 fn test_credentials_debug_redacts_token() {
593 let creds = Credentials::azure_token("supersecrettoken");
594 let debug = format!("{creds:?}");
595 assert!(!debug.contains("supersecrettoken"));
596 assert!(debug.contains("REDACTED"));
597 }
598
599 #[cfg(feature = "cert-auth")]
600 #[test]
601 fn test_credentials_certificate_constructor_and_debug() {
602 let creds =
603 Credentials::certificate("tenant-1", "client-1", "/path/app.pfx", Some("pw".into()));
604 assert!(!creds.is_sql_auth());
605 assert!(!creds.is_azure_ad());
609 assert_eq!(creds.method_name(), "Certificate Authentication");
610 match &creds {
611 Credentials::Certificate {
612 tenant_id,
613 client_id,
614 cert_path,
615 ..
616 } => {
617 assert_eq!(tenant_id.as_ref(), "tenant-1");
618 assert_eq!(client_id.as_ref(), "client-1");
619 assert_eq!(cert_path.as_ref(), "/path/app.pfx");
620 }
621 _ => panic!("Expected Certificate variant"),
622 }
623 let debug = format!("{creds:?}");
624 assert!(debug.contains("tenant-1"));
625 assert!(debug.contains("/path/app.pfx"));
626 assert!(!debug.contains("pw"));
627 assert!(debug.contains("REDACTED"));
628 }
629
630 #[cfg(feature = "azure-identity")]
631 #[test]
632 fn test_credentials_azure_default() {
633 let creds = Credentials::azure_default();
634 assert!(creds.is_azure_ad());
635 assert!(!creds.is_sql_auth());
636 assert!(matches!(creds, Credentials::AzureDefault));
637 assert_eq!(creds.method_name(), "Azure Default Credential Chain");
638 assert_eq!(format!("{creds:?}"), "AzureDefault");
639 }
640
641 #[cfg(feature = "zeroize")]
642 mod zeroize_tests {
643 use super::*;
644
645 #[test]
646 fn test_secret_string_creation() {
647 let secret = SecretString::new("my-password");
648 assert_eq!(secret.expose_secret(), "my-password");
649 }
650
651 #[test]
652 fn test_secret_string_zeroize_clears_value() {
653 let mut secret = SecretString::new("super-secret");
654 secret.zeroize();
655 assert!(secret.expose_secret().is_empty());
659 }
660
661 #[test]
662 fn test_secret_string_is_zeroize_on_drop() {
663 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
665 assert_zeroize_on_drop::<SecretString>();
666 }
667
668 #[test]
669 fn test_secret_string_from_string() {
670 let secret: SecretString = String::from("password").into();
671 assert_eq!(secret.expose_secret(), "password");
672 }
673
674 #[test]
675 fn test_secret_string_from_str() {
676 let secret: SecretString = "password".into();
677 assert_eq!(secret.expose_secret(), "password");
678 }
679
680 #[test]
681 fn test_secret_string_debug_redacted() {
682 let secret = SecretString::new("supersecret");
683 let debug = format!("{secret:?}");
684 assert!(!debug.contains("supersecret"));
685 assert!(debug.contains("REDACTED"));
686 }
687
688 #[test]
689 fn test_secret_string_clone() {
690 let secret = SecretString::new("password");
691 let cloned = secret.clone();
692 assert_eq!(cloned.expose_secret(), "password");
693 }
694
695 #[test]
696 fn test_secure_credentials_sql_server() {
697 let creds = SecureCredentials::sql_server("user", "password");
698 assert_eq!(creds.username(), Some("user"));
699 assert_eq!(creds.password(), Some("password"));
700 assert!(creds.token().is_none());
701 }
702
703 #[test]
704 fn test_secure_credentials_azure_token() {
705 let creds = SecureCredentials::azure_token("my-token");
706 assert!(creds.username().is_none());
707 assert!(creds.password().is_none());
708 assert_eq!(creds.token(), Some("my-token"));
709 }
710
711 #[test]
712 fn test_secure_credentials_debug_redacts_password() {
713 let creds = SecureCredentials::sql_server("user", "supersecret");
714 let debug = format!("{creds:?}");
715 assert!(debug.contains("user"));
716 assert!(!debug.contains("supersecret"));
717 assert!(debug.contains("REDACTED"));
718 }
719
720 #[test]
721 fn test_secure_credentials_debug_redacts_token() {
722 let creds = SecureCredentials::azure_token("supersecrettoken");
723 let debug = format!("{creds:?}");
724 assert!(!debug.contains("supersecrettoken"));
725 assert!(debug.contains("REDACTED"));
726 }
727
728 #[test]
729 fn test_secure_credentials_from_credentials() {
730 let creds = Credentials::sql_server("user", "password");
731 let secure: SecureCredentials = creds.into();
732 assert_eq!(secure.username(), Some("user"));
733 assert_eq!(secure.password(), Some("password"));
734 }
735
736 #[test]
737 fn test_secure_credentials_clone() {
738 let creds = SecureCredentials::sql_server("user", "password");
739 let cloned = creds.clone();
740 assert_eq!(cloned.username(), Some("user"));
741 assert_eq!(cloned.password(), Some("password"));
742 }
743 }
744}