1use std::borrow::Cow;
8
9#[cfg(feature = "zeroize")]
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12#[derive(Clone)]
17pub enum Credentials {
18 SqlServer {
20 username: Cow<'static, str>,
22 password: Cow<'static, str>,
24 },
25
26 AzureAccessToken {
28 token: Cow<'static, str>,
30 },
31
32 #[cfg(feature = "azure-identity")]
34 AzureManagedIdentity {
35 client_id: Option<Cow<'static, str>>,
37 },
38
39 #[cfg(feature = "azure-identity")]
41 AzureServicePrincipal {
42 tenant_id: Cow<'static, str>,
44 client_id: Cow<'static, str>,
46 client_secret: Cow<'static, str>,
48 },
49
50 #[cfg(feature = "integrated-auth")]
52 Integrated,
53
54 #[cfg(feature = "cert-auth")]
56 Certificate {
57 cert_path: Cow<'static, str>,
59 password: Option<Cow<'static, str>>,
61 },
62}
63
64impl Credentials {
65 pub fn sql_server(
67 username: impl Into<Cow<'static, str>>,
68 password: impl Into<Cow<'static, str>>,
69 ) -> Self {
70 Self::SqlServer {
71 username: username.into(),
72 password: password.into(),
73 }
74 }
75
76 pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
78 Self::AzureAccessToken {
79 token: token.into(),
80 }
81 }
82
83 #[must_use]
85 pub fn is_sql_auth(&self) -> bool {
86 matches!(self, Self::SqlServer { .. })
87 }
88
89 #[must_use]
91 pub fn is_azure_ad(&self) -> bool {
92 #[allow(clippy::match_like_matches_macro)]
93 match self {
94 Self::AzureAccessToken { .. } => true,
95 #[cfg(feature = "azure-identity")]
96 Self::AzureManagedIdentity { .. } | Self::AzureServicePrincipal { .. } => true,
97 _ => false,
98 }
99 }
100
101 #[must_use]
103 pub fn method_name(&self) -> &'static str {
104 match self {
105 Self::SqlServer { .. } => "SQL Server Authentication",
106 Self::AzureAccessToken { .. } => "Azure AD Access Token",
107 #[cfg(feature = "azure-identity")]
108 Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
109 #[cfg(feature = "azure-identity")]
110 Self::AzureServicePrincipal { .. } => "Azure Service Principal",
111 #[cfg(feature = "integrated-auth")]
112 Self::Integrated => "Integrated Authentication",
113 #[cfg(feature = "cert-auth")]
114 Self::Certificate { .. } => "Certificate Authentication",
115 }
116 }
117}
118
119impl std::fmt::Debug for Credentials {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
123 Self::SqlServer { username, .. } => f
124 .debug_struct("SqlServer")
125 .field("username", username)
126 .field("password", &"[REDACTED]")
127 .finish(),
128 Self::AzureAccessToken { .. } => f
129 .debug_struct("AzureAccessToken")
130 .field("token", &"[REDACTED]")
131 .finish(),
132 #[cfg(feature = "azure-identity")]
133 Self::AzureManagedIdentity { client_id } => f
134 .debug_struct("AzureManagedIdentity")
135 .field("client_id", client_id)
136 .finish(),
137 #[cfg(feature = "azure-identity")]
138 Self::AzureServicePrincipal {
139 tenant_id,
140 client_id,
141 ..
142 } => f
143 .debug_struct("AzureServicePrincipal")
144 .field("tenant_id", tenant_id)
145 .field("client_id", client_id)
146 .field("client_secret", &"[REDACTED]")
147 .finish(),
148 #[cfg(feature = "integrated-auth")]
149 Self::Integrated => f.debug_struct("Integrated").finish(),
150 #[cfg(feature = "cert-auth")]
151 Self::Certificate { cert_path, .. } => f
152 .debug_struct("Certificate")
153 .field("cert_path", cert_path)
154 .field("password", &"[REDACTED]")
155 .finish(),
156 }
157 }
158}
159
160#[cfg(feature = "zeroize")]
170#[derive(Clone, Zeroize, ZeroizeOnDrop)]
171pub struct SecretString(String);
172
173#[cfg(feature = "zeroize")]
174impl SecretString {
175 pub fn new(value: impl Into<String>) -> Self {
177 Self(value.into())
178 }
179
180 #[must_use]
187 pub fn expose_secret(&self) -> &str {
188 &self.0
189 }
190}
191
192#[cfg(feature = "zeroize")]
193impl std::fmt::Debug for SecretString {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 write!(f, "[REDACTED]")
196 }
197}
198
199#[cfg(feature = "zeroize")]
200impl From<String> for SecretString {
201 fn from(s: String) -> Self {
202 Self::new(s)
203 }
204}
205
206#[cfg(feature = "zeroize")]
207impl From<&str> for SecretString {
208 fn from(s: &str) -> Self {
209 Self::new(s)
210 }
211}
212
213#[cfg(feature = "zeroize")]
228#[derive(Clone, Zeroize, ZeroizeOnDrop)]
229pub struct SecureCredentials {
230 kind: SecureCredentialKind,
231}
232
233#[cfg(feature = "zeroize")]
234#[derive(Clone, Zeroize, ZeroizeOnDrop)]
235enum SecureCredentialKind {
236 SqlServer {
237 username: String,
238 password: SecretString,
239 },
240 AzureAccessToken {
241 token: SecretString,
242 },
243 #[cfg(feature = "azure-identity")]
244 AzureManagedIdentity {
245 client_id: Option<String>,
246 },
247 #[cfg(feature = "azure-identity")]
248 AzureServicePrincipal {
249 tenant_id: String,
250 client_id: String,
251 client_secret: SecretString,
252 },
253 #[cfg(feature = "integrated-auth")]
254 Integrated,
255 #[cfg(feature = "cert-auth")]
256 Certificate {
257 cert_path: String,
258 password: Option<SecretString>,
259 },
260}
261
262#[cfg(feature = "zeroize")]
263impl SecureCredentials {
264 pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
266 Self {
267 kind: SecureCredentialKind::SqlServer {
268 username: username.into(),
269 password: SecretString::new(password),
270 },
271 }
272 }
273
274 pub fn azure_token(token: impl Into<String>) -> Self {
276 Self {
277 kind: SecureCredentialKind::AzureAccessToken {
278 token: SecretString::new(token),
279 },
280 }
281 }
282
283 #[must_use]
285 pub fn is_sql_auth(&self) -> bool {
286 matches!(self.kind, SecureCredentialKind::SqlServer { .. })
287 }
288
289 #[must_use]
291 pub fn is_azure_ad(&self) -> bool {
292 #[allow(clippy::match_like_matches_macro)]
293 match &self.kind {
294 SecureCredentialKind::AzureAccessToken { .. } => true,
295 #[cfg(feature = "azure-identity")]
296 SecureCredentialKind::AzureManagedIdentity { .. }
297 | SecureCredentialKind::AzureServicePrincipal { .. } => true,
298 _ => false,
299 }
300 }
301
302 #[must_use]
304 pub fn method_name(&self) -> &'static str {
305 match &self.kind {
306 SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
307 SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
308 #[cfg(feature = "azure-identity")]
309 SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
310 #[cfg(feature = "azure-identity")]
311 SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
312 #[cfg(feature = "integrated-auth")]
313 SecureCredentialKind::Integrated => "Integrated Authentication",
314 #[cfg(feature = "cert-auth")]
315 SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
316 }
317 }
318
319 #[must_use]
323 pub fn username(&self) -> Option<&str> {
324 match &self.kind {
325 SecureCredentialKind::SqlServer { username, .. } => Some(username),
326 _ => None,
327 }
328 }
329
330 #[must_use]
339 pub fn password(&self) -> Option<&str> {
340 match &self.kind {
341 SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
342 _ => None,
343 }
344 }
345
346 #[must_use]
355 pub fn token(&self) -> Option<&str> {
356 match &self.kind {
357 SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
358 _ => None,
359 }
360 }
361}
362
363#[cfg(feature = "zeroize")]
364impl std::fmt::Debug for SecureCredentials {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 match &self.kind {
367 SecureCredentialKind::SqlServer { username, .. } => f
368 .debug_struct("SecureCredentials::SqlServer")
369 .field("username", username)
370 .field("password", &"[REDACTED]")
371 .finish(),
372 SecureCredentialKind::AzureAccessToken { .. } => f
373 .debug_struct("SecureCredentials::AzureAccessToken")
374 .field("token", &"[REDACTED]")
375 .finish(),
376 #[cfg(feature = "azure-identity")]
377 SecureCredentialKind::AzureManagedIdentity { client_id } => f
378 .debug_struct("SecureCredentials::AzureManagedIdentity")
379 .field("client_id", client_id)
380 .finish(),
381 #[cfg(feature = "azure-identity")]
382 SecureCredentialKind::AzureServicePrincipal {
383 tenant_id,
384 client_id,
385 ..
386 } => f
387 .debug_struct("SecureCredentials::AzureServicePrincipal")
388 .field("tenant_id", tenant_id)
389 .field("client_id", client_id)
390 .field("client_secret", &"[REDACTED]")
391 .finish(),
392 #[cfg(feature = "integrated-auth")]
393 SecureCredentialKind::Integrated => {
394 f.debug_struct("SecureCredentials::Integrated").finish()
395 }
396 #[cfg(feature = "cert-auth")]
397 SecureCredentialKind::Certificate { cert_path, .. } => f
398 .debug_struct("SecureCredentials::Certificate")
399 .field("cert_path", cert_path)
400 .field("password", &"[REDACTED]")
401 .finish(),
402 }
403 }
404}
405
406#[cfg(feature = "zeroize")]
408impl From<Credentials> for SecureCredentials {
409 fn from(creds: Credentials) -> Self {
410 match creds {
411 Credentials::SqlServer { username, password } => {
412 SecureCredentials::sql_server(username.into_owned(), password.into_owned())
413 }
414 Credentials::AzureAccessToken { token } => {
415 SecureCredentials::azure_token(token.into_owned())
416 }
417 #[cfg(feature = "azure-identity")]
418 Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
419 kind: SecureCredentialKind::AzureManagedIdentity {
420 client_id: client_id.map(|c| c.into_owned()),
421 },
422 },
423 #[cfg(feature = "azure-identity")]
424 Credentials::AzureServicePrincipal {
425 tenant_id,
426 client_id,
427 client_secret,
428 } => SecureCredentials {
429 kind: SecureCredentialKind::AzureServicePrincipal {
430 tenant_id: tenant_id.into_owned(),
431 client_id: client_id.into_owned(),
432 client_secret: SecretString::new(client_secret.into_owned()),
433 },
434 },
435 #[cfg(feature = "integrated-auth")]
436 Credentials::Integrated => SecureCredentials {
437 kind: SecureCredentialKind::Integrated,
438 },
439 #[cfg(feature = "cert-auth")]
440 Credentials::Certificate {
441 cert_path,
442 password,
443 } => SecureCredentials {
444 kind: SecureCredentialKind::Certificate {
445 cert_path: cert_path.into_owned(),
446 password: password.map(|p| SecretString::new(p.into_owned())),
447 },
448 },
449 }
450 }
451}
452
453#[cfg(test)]
454#[allow(clippy::panic)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_credentials_sql_server() {
460 let creds = Credentials::sql_server("user", "password");
461 assert!(creds.is_sql_auth());
462 assert!(!creds.is_azure_ad());
463 match creds {
464 Credentials::SqlServer { username, password } => {
465 assert_eq!(username.as_ref(), "user");
466 assert_eq!(password.as_ref(), "password");
467 }
468 _ => panic!("Expected SqlServer variant"),
469 }
470 }
471
472 #[test]
473 fn test_credentials_azure_token() {
474 let creds = Credentials::azure_token("my-token");
475 assert!(!creds.is_sql_auth());
476 assert!(creds.is_azure_ad());
477 match creds {
478 Credentials::AzureAccessToken { token } => {
479 assert_eq!(token.as_ref(), "my-token");
480 }
481 _ => panic!("Expected AzureAccessToken variant"),
482 }
483 }
484
485 #[test]
486 fn test_credentials_debug_redacts_password() {
487 let creds = Credentials::sql_server("user", "supersecret");
488 let debug = format!("{:?}", creds);
489 assert!(debug.contains("user"));
490 assert!(!debug.contains("supersecret"));
491 assert!(debug.contains("REDACTED"));
492 }
493
494 #[test]
495 fn test_credentials_debug_redacts_token() {
496 let creds = Credentials::azure_token("supersecrettoken");
497 let debug = format!("{:?}", creds);
498 assert!(!debug.contains("supersecrettoken"));
499 assert!(debug.contains("REDACTED"));
500 }
501
502 #[cfg(feature = "zeroize")]
503 mod zeroize_tests {
504 use super::*;
505
506 #[test]
507 fn test_secret_string_creation() {
508 let secret = SecretString::new("my-password");
509 assert_eq!(secret.expose_secret(), "my-password");
510 }
511
512 #[test]
513 fn test_secret_string_from_string() {
514 let secret: SecretString = String::from("password").into();
515 assert_eq!(secret.expose_secret(), "password");
516 }
517
518 #[test]
519 fn test_secret_string_from_str() {
520 let secret: SecretString = "password".into();
521 assert_eq!(secret.expose_secret(), "password");
522 }
523
524 #[test]
525 fn test_secret_string_debug_redacted() {
526 let secret = SecretString::new("supersecret");
527 let debug = format!("{:?}", secret);
528 assert!(!debug.contains("supersecret"));
529 assert!(debug.contains("REDACTED"));
530 }
531
532 #[test]
533 fn test_secret_string_clone() {
534 let secret = SecretString::new("password");
535 let cloned = secret.clone();
536 assert_eq!(cloned.expose_secret(), "password");
537 }
538
539 #[test]
540 fn test_secure_credentials_sql_server() {
541 let creds = SecureCredentials::sql_server("user", "password");
542 assert_eq!(creds.username(), Some("user"));
543 assert_eq!(creds.password(), Some("password"));
544 assert!(creds.token().is_none());
545 }
546
547 #[test]
548 fn test_secure_credentials_azure_token() {
549 let creds = SecureCredentials::azure_token("my-token");
550 assert!(creds.username().is_none());
551 assert!(creds.password().is_none());
552 assert_eq!(creds.token(), Some("my-token"));
553 }
554
555 #[test]
556 fn test_secure_credentials_debug_redacts_password() {
557 let creds = SecureCredentials::sql_server("user", "supersecret");
558 let debug = format!("{:?}", creds);
559 assert!(debug.contains("user"));
560 assert!(!debug.contains("supersecret"));
561 assert!(debug.contains("REDACTED"));
562 }
563
564 #[test]
565 fn test_secure_credentials_debug_redacts_token() {
566 let creds = SecureCredentials::azure_token("supersecrettoken");
567 let debug = format!("{:?}", creds);
568 assert!(!debug.contains("supersecrettoken"));
569 assert!(debug.contains("REDACTED"));
570 }
571
572 #[test]
573 fn test_secure_credentials_from_credentials() {
574 let creds = Credentials::sql_server("user", "password");
575 let secure: SecureCredentials = creds.into();
576 assert_eq!(secure.username(), Some("user"));
577 assert_eq!(secure.password(), Some("password"));
578 }
579
580 #[test]
581 fn test_secure_credentials_clone() {
582 let creds = SecureCredentials::sql_server("user", "password");
583 let cloned = creds.clone();
584 assert_eq!(cloned.username(), Some("user"));
585 assert_eq!(cloned.password(), Some("password"));
586 }
587 }
588}