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(any(feature = "integrated-auth", feature = "sspi-auth"))]
53 Integrated,
54
55 #[cfg(feature = "cert-auth")]
57 Certificate {
58 cert_path: Cow<'static, str>,
60 password: Option<Cow<'static, str>>,
62 },
63}
64
65impl Credentials {
66 pub fn sql_server(
68 username: impl Into<Cow<'static, str>>,
69 password: impl Into<Cow<'static, str>>,
70 ) -> Self {
71 Self::SqlServer {
72 username: username.into(),
73 password: password.into(),
74 }
75 }
76
77 pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
79 Self::AzureAccessToken {
80 token: token.into(),
81 }
82 }
83
84 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
88 #[must_use]
89 pub fn integrated() -> Self {
90 Self::Integrated
91 }
92
93 #[must_use]
95 pub fn is_sql_auth(&self) -> bool {
96 matches!(self, Self::SqlServer { .. })
97 }
98
99 #[must_use]
101 pub fn is_azure_ad(&self) -> bool {
102 #[allow(clippy::match_like_matches_macro)]
103 match self {
104 Self::AzureAccessToken { .. } => true,
105 #[cfg(feature = "azure-identity")]
106 Self::AzureManagedIdentity { .. } | Self::AzureServicePrincipal { .. } => true,
107 _ => false,
108 }
109 }
110
111 #[must_use]
113 pub fn method_name(&self) -> &'static str {
114 match self {
115 Self::SqlServer { .. } => "SQL Server Authentication",
116 Self::AzureAccessToken { .. } => "Azure AD Access Token",
117 #[cfg(feature = "azure-identity")]
118 Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
119 #[cfg(feature = "azure-identity")]
120 Self::AzureServicePrincipal { .. } => "Azure Service Principal",
121 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
122 Self::Integrated => "Integrated Authentication",
123 #[cfg(feature = "cert-auth")]
124 Self::Certificate { .. } => "Certificate Authentication",
125 }
126 }
127}
128
129impl std::fmt::Debug for Credentials {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
133 Self::SqlServer { username, .. } => f
134 .debug_struct("SqlServer")
135 .field("username", username)
136 .field("password", &"[REDACTED]")
137 .finish(),
138 Self::AzureAccessToken { .. } => f
139 .debug_struct("AzureAccessToken")
140 .field("token", &"[REDACTED]")
141 .finish(),
142 #[cfg(feature = "azure-identity")]
143 Self::AzureManagedIdentity { client_id } => f
144 .debug_struct("AzureManagedIdentity")
145 .field("client_id", client_id)
146 .finish(),
147 #[cfg(feature = "azure-identity")]
148 Self::AzureServicePrincipal {
149 tenant_id,
150 client_id,
151 ..
152 } => f
153 .debug_struct("AzureServicePrincipal")
154 .field("tenant_id", tenant_id)
155 .field("client_id", client_id)
156 .field("client_secret", &"[REDACTED]")
157 .finish(),
158 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
159 Self::Integrated => f.debug_struct("Integrated").finish(),
160 #[cfg(feature = "cert-auth")]
161 Self::Certificate { cert_path, .. } => f
162 .debug_struct("Certificate")
163 .field("cert_path", cert_path)
164 .field("password", &"[REDACTED]")
165 .finish(),
166 }
167 }
168}
169
170#[cfg(feature = "zeroize")]
180#[derive(Clone, Zeroize, ZeroizeOnDrop)]
181pub struct SecretString(String);
182
183#[cfg(feature = "zeroize")]
184impl SecretString {
185 pub fn new(value: impl Into<String>) -> Self {
187 Self(value.into())
188 }
189
190 #[must_use]
197 pub fn expose_secret(&self) -> &str {
198 &self.0
199 }
200}
201
202#[cfg(feature = "zeroize")]
203impl std::fmt::Debug for SecretString {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "[REDACTED]")
206 }
207}
208
209#[cfg(feature = "zeroize")]
210impl From<String> for SecretString {
211 fn from(s: String) -> Self {
212 Self::new(s)
213 }
214}
215
216#[cfg(feature = "zeroize")]
217impl From<&str> for SecretString {
218 fn from(s: &str) -> Self {
219 Self::new(s)
220 }
221}
222
223#[cfg(feature = "zeroize")]
238#[derive(Clone, Zeroize, ZeroizeOnDrop)]
239pub struct SecureCredentials {
240 kind: SecureCredentialKind,
241}
242
243#[cfg(feature = "zeroize")]
244#[derive(Clone, Zeroize, ZeroizeOnDrop)]
245enum SecureCredentialKind {
246 SqlServer {
247 username: String,
248 password: SecretString,
249 },
250 AzureAccessToken {
251 token: SecretString,
252 },
253 #[cfg(feature = "azure-identity")]
254 AzureManagedIdentity {
255 client_id: Option<String>,
256 },
257 #[cfg(feature = "azure-identity")]
258 AzureServicePrincipal {
259 tenant_id: String,
260 client_id: String,
261 client_secret: SecretString,
262 },
263 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
264 Integrated,
265 #[cfg(feature = "cert-auth")]
266 Certificate {
267 cert_path: String,
268 password: Option<SecretString>,
269 },
270}
271
272#[cfg(feature = "zeroize")]
273impl SecureCredentials {
274 pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
276 Self {
277 kind: SecureCredentialKind::SqlServer {
278 username: username.into(),
279 password: SecretString::new(password),
280 },
281 }
282 }
283
284 pub fn azure_token(token: impl Into<String>) -> Self {
286 Self {
287 kind: SecureCredentialKind::AzureAccessToken {
288 token: SecretString::new(token),
289 },
290 }
291 }
292
293 #[must_use]
295 pub fn is_sql_auth(&self) -> bool {
296 matches!(self.kind, SecureCredentialKind::SqlServer { .. })
297 }
298
299 #[must_use]
301 pub fn is_azure_ad(&self) -> bool {
302 #[allow(clippy::match_like_matches_macro)]
303 match &self.kind {
304 SecureCredentialKind::AzureAccessToken { .. } => true,
305 #[cfg(feature = "azure-identity")]
306 SecureCredentialKind::AzureManagedIdentity { .. }
307 | SecureCredentialKind::AzureServicePrincipal { .. } => true,
308 _ => false,
309 }
310 }
311
312 #[must_use]
314 pub fn method_name(&self) -> &'static str {
315 match &self.kind {
316 SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
317 SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
318 #[cfg(feature = "azure-identity")]
319 SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
320 #[cfg(feature = "azure-identity")]
321 SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
322 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
323 SecureCredentialKind::Integrated => "Integrated Authentication",
324 #[cfg(feature = "cert-auth")]
325 SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
326 }
327 }
328
329 #[must_use]
333 pub fn username(&self) -> Option<&str> {
334 match &self.kind {
335 SecureCredentialKind::SqlServer { username, .. } => Some(username),
336 _ => None,
337 }
338 }
339
340 #[must_use]
349 pub fn password(&self) -> Option<&str> {
350 match &self.kind {
351 SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
352 _ => None,
353 }
354 }
355
356 #[must_use]
365 pub fn token(&self) -> Option<&str> {
366 match &self.kind {
367 SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
368 _ => None,
369 }
370 }
371}
372
373#[cfg(feature = "zeroize")]
374impl std::fmt::Debug for SecureCredentials {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 match &self.kind {
377 SecureCredentialKind::SqlServer { username, .. } => f
378 .debug_struct("SecureCredentials::SqlServer")
379 .field("username", username)
380 .field("password", &"[REDACTED]")
381 .finish(),
382 SecureCredentialKind::AzureAccessToken { .. } => f
383 .debug_struct("SecureCredentials::AzureAccessToken")
384 .field("token", &"[REDACTED]")
385 .finish(),
386 #[cfg(feature = "azure-identity")]
387 SecureCredentialKind::AzureManagedIdentity { client_id } => f
388 .debug_struct("SecureCredentials::AzureManagedIdentity")
389 .field("client_id", client_id)
390 .finish(),
391 #[cfg(feature = "azure-identity")]
392 SecureCredentialKind::AzureServicePrincipal {
393 tenant_id,
394 client_id,
395 ..
396 } => f
397 .debug_struct("SecureCredentials::AzureServicePrincipal")
398 .field("tenant_id", tenant_id)
399 .field("client_id", client_id)
400 .field("client_secret", &"[REDACTED]")
401 .finish(),
402 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
403 SecureCredentialKind::Integrated => {
404 f.debug_struct("SecureCredentials::Integrated").finish()
405 }
406 #[cfg(feature = "cert-auth")]
407 SecureCredentialKind::Certificate { cert_path, .. } => f
408 .debug_struct("SecureCredentials::Certificate")
409 .field("cert_path", cert_path)
410 .field("password", &"[REDACTED]")
411 .finish(),
412 }
413 }
414}
415
416#[cfg(feature = "zeroize")]
418impl From<Credentials> for SecureCredentials {
419 fn from(creds: Credentials) -> Self {
420 match creds {
421 Credentials::SqlServer { username, password } => {
422 SecureCredentials::sql_server(username.into_owned(), password.into_owned())
423 }
424 Credentials::AzureAccessToken { token } => {
425 SecureCredentials::azure_token(token.into_owned())
426 }
427 #[cfg(feature = "azure-identity")]
428 Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
429 kind: SecureCredentialKind::AzureManagedIdentity {
430 client_id: client_id.map(|c| c.into_owned()),
431 },
432 },
433 #[cfg(feature = "azure-identity")]
434 Credentials::AzureServicePrincipal {
435 tenant_id,
436 client_id,
437 client_secret,
438 } => SecureCredentials {
439 kind: SecureCredentialKind::AzureServicePrincipal {
440 tenant_id: tenant_id.into_owned(),
441 client_id: client_id.into_owned(),
442 client_secret: SecretString::new(client_secret.into_owned()),
443 },
444 },
445 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
446 Credentials::Integrated => SecureCredentials {
447 kind: SecureCredentialKind::Integrated,
448 },
449 #[cfg(feature = "cert-auth")]
450 Credentials::Certificate {
451 cert_path,
452 password,
453 } => SecureCredentials {
454 kind: SecureCredentialKind::Certificate {
455 cert_path: cert_path.into_owned(),
456 password: password.map(|p| SecretString::new(p.into_owned())),
457 },
458 },
459 }
460 }
461}
462
463#[cfg(test)]
464#[allow(clippy::panic)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_credentials_sql_server() {
470 let creds = Credentials::sql_server("user", "password");
471 assert!(creds.is_sql_auth());
472 assert!(!creds.is_azure_ad());
473 match creds {
474 Credentials::SqlServer { username, password } => {
475 assert_eq!(username.as_ref(), "user");
476 assert_eq!(password.as_ref(), "password");
477 }
478 _ => panic!("Expected SqlServer variant"),
479 }
480 }
481
482 #[test]
483 fn test_credentials_azure_token() {
484 let creds = Credentials::azure_token("my-token");
485 assert!(!creds.is_sql_auth());
486 assert!(creds.is_azure_ad());
487 match creds {
488 Credentials::AzureAccessToken { token } => {
489 assert_eq!(token.as_ref(), "my-token");
490 }
491 _ => panic!("Expected AzureAccessToken variant"),
492 }
493 }
494
495 #[test]
496 fn test_credentials_debug_redacts_password() {
497 let creds = Credentials::sql_server("user", "supersecret");
498 let debug = format!("{creds:?}");
499 assert!(debug.contains("user"));
500 assert!(!debug.contains("supersecret"));
501 assert!(debug.contains("REDACTED"));
502 }
503
504 #[test]
505 fn test_credentials_debug_redacts_token() {
506 let creds = Credentials::azure_token("supersecrettoken");
507 let debug = format!("{creds:?}");
508 assert!(!debug.contains("supersecrettoken"));
509 assert!(debug.contains("REDACTED"));
510 }
511
512 #[cfg(feature = "zeroize")]
513 mod zeroize_tests {
514 use super::*;
515
516 #[test]
517 fn test_secret_string_creation() {
518 let secret = SecretString::new("my-password");
519 assert_eq!(secret.expose_secret(), "my-password");
520 }
521
522 #[test]
523 fn test_secret_string_from_string() {
524 let secret: SecretString = String::from("password").into();
525 assert_eq!(secret.expose_secret(), "password");
526 }
527
528 #[test]
529 fn test_secret_string_from_str() {
530 let secret: SecretString = "password".into();
531 assert_eq!(secret.expose_secret(), "password");
532 }
533
534 #[test]
535 fn test_secret_string_debug_redacted() {
536 let secret = SecretString::new("supersecret");
537 let debug = format!("{secret:?}");
538 assert!(!debug.contains("supersecret"));
539 assert!(debug.contains("REDACTED"));
540 }
541
542 #[test]
543 fn test_secret_string_clone() {
544 let secret = SecretString::new("password");
545 let cloned = secret.clone();
546 assert_eq!(cloned.expose_secret(), "password");
547 }
548
549 #[test]
550 fn test_secure_credentials_sql_server() {
551 let creds = SecureCredentials::sql_server("user", "password");
552 assert_eq!(creds.username(), Some("user"));
553 assert_eq!(creds.password(), Some("password"));
554 assert!(creds.token().is_none());
555 }
556
557 #[test]
558 fn test_secure_credentials_azure_token() {
559 let creds = SecureCredentials::azure_token("my-token");
560 assert!(creds.username().is_none());
561 assert!(creds.password().is_none());
562 assert_eq!(creds.token(), Some("my-token"));
563 }
564
565 #[test]
566 fn test_secure_credentials_debug_redacts_password() {
567 let creds = SecureCredentials::sql_server("user", "supersecret");
568 let debug = format!("{creds:?}");
569 assert!(debug.contains("user"));
570 assert!(!debug.contains("supersecret"));
571 assert!(debug.contains("REDACTED"));
572 }
573
574 #[test]
575 fn test_secure_credentials_debug_redacts_token() {
576 let creds = SecureCredentials::azure_token("supersecrettoken");
577 let debug = format!("{creds:?}");
578 assert!(!debug.contains("supersecrettoken"));
579 assert!(debug.contains("REDACTED"));
580 }
581
582 #[test]
583 fn test_secure_credentials_from_credentials() {
584 let creds = Credentials::sql_server("user", "password");
585 let secure: SecureCredentials = creds.into();
586 assert_eq!(secure.username(), Some("user"));
587 assert_eq!(secure.password(), Some("password"));
588 }
589
590 #[test]
591 fn test_secure_credentials_clone() {
592 let creds = SecureCredentials::sql_server("user", "password");
593 let cloned = creds.clone();
594 assert_eq!(cloned.username(), Some("user"));
595 assert_eq!(cloned.password(), Some("password"));
596 }
597 }
598}