use std::borrow::Cow;
#[cfg(feature = "zeroize")]
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Clone)]
#[non_exhaustive]
pub enum Credentials {
SqlServer {
username: Cow<'static, str>,
password: Cow<'static, str>,
},
AzureAccessToken {
token: Cow<'static, str>,
},
#[cfg(feature = "azure-identity")]
AzureManagedIdentity {
client_id: Option<Cow<'static, str>>,
},
#[cfg(feature = "azure-identity")]
AzureServicePrincipal {
tenant_id: Cow<'static, str>,
client_id: Cow<'static, str>,
client_secret: Cow<'static, str>,
},
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
Integrated,
#[cfg(feature = "cert-auth")]
Certificate {
cert_path: Cow<'static, str>,
password: Option<Cow<'static, str>>,
},
}
impl Credentials {
pub fn sql_server(
username: impl Into<Cow<'static, str>>,
password: impl Into<Cow<'static, str>>,
) -> Self {
Self::SqlServer {
username: username.into(),
password: password.into(),
}
}
pub fn azure_token(token: impl Into<Cow<'static, str>>) -> Self {
Self::AzureAccessToken {
token: token.into(),
}
}
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
#[must_use]
pub fn integrated() -> Self {
Self::Integrated
}
#[must_use]
pub fn is_sql_auth(&self) -> bool {
matches!(self, Self::SqlServer { .. })
}
#[must_use]
pub fn is_azure_ad(&self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match self {
Self::AzureAccessToken { .. } => true,
#[cfg(feature = "azure-identity")]
Self::AzureManagedIdentity { .. } | Self::AzureServicePrincipal { .. } => true,
_ => false,
}
}
#[must_use]
pub fn method_name(&self) -> &'static str {
match self {
Self::SqlServer { .. } => "SQL Server Authentication",
Self::AzureAccessToken { .. } => "Azure AD Access Token",
#[cfg(feature = "azure-identity")]
Self::AzureManagedIdentity { .. } => "Azure Managed Identity",
#[cfg(feature = "azure-identity")]
Self::AzureServicePrincipal { .. } => "Azure Service Principal",
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
Self::Integrated => "Integrated Authentication",
#[cfg(feature = "cert-auth")]
Self::Certificate { .. } => "Certificate Authentication",
}
}
}
impl std::fmt::Debug for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SqlServer { username, .. } => f
.debug_struct("SqlServer")
.field("username", username)
.field("password", &"[REDACTED]")
.finish(),
Self::AzureAccessToken { .. } => f
.debug_struct("AzureAccessToken")
.field("token", &"[REDACTED]")
.finish(),
#[cfg(feature = "azure-identity")]
Self::AzureManagedIdentity { client_id } => f
.debug_struct("AzureManagedIdentity")
.field("client_id", client_id)
.finish(),
#[cfg(feature = "azure-identity")]
Self::AzureServicePrincipal {
tenant_id,
client_id,
..
} => f
.debug_struct("AzureServicePrincipal")
.field("tenant_id", tenant_id)
.field("client_id", client_id)
.field("client_secret", &"[REDACTED]")
.finish(),
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
Self::Integrated => f.debug_struct("Integrated").finish(),
#[cfg(feature = "cert-auth")]
Self::Certificate { cert_path, .. } => f
.debug_struct("Certificate")
.field("cert_path", cert_path)
.field("password", &"[REDACTED]")
.finish(),
}
}
}
#[cfg(feature = "zeroize")]
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecretString(String);
#[cfg(feature = "zeroize")]
impl SecretString {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn expose_secret(&self) -> &str {
&self.0
}
}
#[cfg(feature = "zeroize")]
impl std::fmt::Debug for SecretString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[REDACTED]")
}
}
#[cfg(feature = "zeroize")]
impl From<String> for SecretString {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[cfg(feature = "zeroize")]
impl From<&str> for SecretString {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[cfg(feature = "zeroize")]
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SecureCredentials {
kind: SecureCredentialKind,
}
#[cfg(feature = "zeroize")]
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
enum SecureCredentialKind {
SqlServer {
username: String,
password: SecretString,
},
AzureAccessToken {
token: SecretString,
},
#[cfg(feature = "azure-identity")]
AzureManagedIdentity {
client_id: Option<String>,
},
#[cfg(feature = "azure-identity")]
AzureServicePrincipal {
tenant_id: String,
client_id: String,
client_secret: SecretString,
},
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
Integrated,
#[cfg(feature = "cert-auth")]
Certificate {
cert_path: String,
password: Option<SecretString>,
},
}
#[cfg(feature = "zeroize")]
impl SecureCredentials {
pub fn sql_server(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
kind: SecureCredentialKind::SqlServer {
username: username.into(),
password: SecretString::new(password),
},
}
}
pub fn azure_token(token: impl Into<String>) -> Self {
Self {
kind: SecureCredentialKind::AzureAccessToken {
token: SecretString::new(token),
},
}
}
#[must_use]
pub fn is_sql_auth(&self) -> bool {
matches!(self.kind, SecureCredentialKind::SqlServer { .. })
}
#[must_use]
pub fn is_azure_ad(&self) -> bool {
#[allow(clippy::match_like_matches_macro)]
match &self.kind {
SecureCredentialKind::AzureAccessToken { .. } => true,
#[cfg(feature = "azure-identity")]
SecureCredentialKind::AzureManagedIdentity { .. }
| SecureCredentialKind::AzureServicePrincipal { .. } => true,
_ => false,
}
}
#[must_use]
pub fn method_name(&self) -> &'static str {
match &self.kind {
SecureCredentialKind::SqlServer { .. } => "SQL Server Authentication",
SecureCredentialKind::AzureAccessToken { .. } => "Azure AD Access Token",
#[cfg(feature = "azure-identity")]
SecureCredentialKind::AzureManagedIdentity { .. } => "Azure Managed Identity",
#[cfg(feature = "azure-identity")]
SecureCredentialKind::AzureServicePrincipal { .. } => "Azure Service Principal",
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
SecureCredentialKind::Integrated => "Integrated Authentication",
#[cfg(feature = "cert-auth")]
SecureCredentialKind::Certificate { .. } => "Certificate Authentication",
}
}
#[must_use]
pub fn username(&self) -> Option<&str> {
match &self.kind {
SecureCredentialKind::SqlServer { username, .. } => Some(username),
_ => None,
}
}
#[must_use]
pub fn password(&self) -> Option<&str> {
match &self.kind {
SecureCredentialKind::SqlServer { password, .. } => Some(password.expose_secret()),
_ => None,
}
}
#[must_use]
pub fn token(&self) -> Option<&str> {
match &self.kind {
SecureCredentialKind::AzureAccessToken { token } => Some(token.expose_secret()),
_ => None,
}
}
}
#[cfg(feature = "zeroize")]
impl std::fmt::Debug for SecureCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
SecureCredentialKind::SqlServer { username, .. } => f
.debug_struct("SecureCredentials::SqlServer")
.field("username", username)
.field("password", &"[REDACTED]")
.finish(),
SecureCredentialKind::AzureAccessToken { .. } => f
.debug_struct("SecureCredentials::AzureAccessToken")
.field("token", &"[REDACTED]")
.finish(),
#[cfg(feature = "azure-identity")]
SecureCredentialKind::AzureManagedIdentity { client_id } => f
.debug_struct("SecureCredentials::AzureManagedIdentity")
.field("client_id", client_id)
.finish(),
#[cfg(feature = "azure-identity")]
SecureCredentialKind::AzureServicePrincipal {
tenant_id,
client_id,
..
} => f
.debug_struct("SecureCredentials::AzureServicePrincipal")
.field("tenant_id", tenant_id)
.field("client_id", client_id)
.field("client_secret", &"[REDACTED]")
.finish(),
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
SecureCredentialKind::Integrated => {
f.debug_struct("SecureCredentials::Integrated").finish()
}
#[cfg(feature = "cert-auth")]
SecureCredentialKind::Certificate { cert_path, .. } => f
.debug_struct("SecureCredentials::Certificate")
.field("cert_path", cert_path)
.field("password", &"[REDACTED]")
.finish(),
}
}
}
#[cfg(feature = "zeroize")]
impl From<Credentials> for SecureCredentials {
fn from(creds: Credentials) -> Self {
match creds {
Credentials::SqlServer { username, password } => {
SecureCredentials::sql_server(username.into_owned(), password.into_owned())
}
Credentials::AzureAccessToken { token } => {
SecureCredentials::azure_token(token.into_owned())
}
#[cfg(feature = "azure-identity")]
Credentials::AzureManagedIdentity { client_id } => SecureCredentials {
kind: SecureCredentialKind::AzureManagedIdentity {
client_id: client_id.map(|c| c.into_owned()),
},
},
#[cfg(feature = "azure-identity")]
Credentials::AzureServicePrincipal {
tenant_id,
client_id,
client_secret,
} => SecureCredentials {
kind: SecureCredentialKind::AzureServicePrincipal {
tenant_id: tenant_id.into_owned(),
client_id: client_id.into_owned(),
client_secret: SecretString::new(client_secret.into_owned()),
},
},
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
Credentials::Integrated => SecureCredentials {
kind: SecureCredentialKind::Integrated,
},
#[cfg(feature = "cert-auth")]
Credentials::Certificate {
cert_path,
password,
} => SecureCredentials {
kind: SecureCredentialKind::Certificate {
cert_path: cert_path.into_owned(),
password: password.map(|p| SecretString::new(p.into_owned())),
},
},
}
}
}
#[cfg(test)]
#[allow(clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_credentials_sql_server() {
let creds = Credentials::sql_server("user", "password");
assert!(creds.is_sql_auth());
assert!(!creds.is_azure_ad());
match creds {
Credentials::SqlServer { username, password } => {
assert_eq!(username.as_ref(), "user");
assert_eq!(password.as_ref(), "password");
}
_ => panic!("Expected SqlServer variant"),
}
}
#[test]
fn test_credentials_azure_token() {
let creds = Credentials::azure_token("my-token");
assert!(!creds.is_sql_auth());
assert!(creds.is_azure_ad());
match creds {
Credentials::AzureAccessToken { token } => {
assert_eq!(token.as_ref(), "my-token");
}
_ => panic!("Expected AzureAccessToken variant"),
}
}
#[test]
fn test_credentials_debug_redacts_password() {
let creds = Credentials::sql_server("user", "supersecret");
let debug = format!("{creds:?}");
assert!(debug.contains("user"));
assert!(!debug.contains("supersecret"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_credentials_debug_redacts_token() {
let creds = Credentials::azure_token("supersecrettoken");
let debug = format!("{creds:?}");
assert!(!debug.contains("supersecrettoken"));
assert!(debug.contains("REDACTED"));
}
#[cfg(feature = "zeroize")]
mod zeroize_tests {
use super::*;
#[test]
fn test_secret_string_creation() {
let secret = SecretString::new("my-password");
assert_eq!(secret.expose_secret(), "my-password");
}
#[test]
fn test_secret_string_from_string() {
let secret: SecretString = String::from("password").into();
assert_eq!(secret.expose_secret(), "password");
}
#[test]
fn test_secret_string_from_str() {
let secret: SecretString = "password".into();
assert_eq!(secret.expose_secret(), "password");
}
#[test]
fn test_secret_string_debug_redacted() {
let secret = SecretString::new("supersecret");
let debug = format!("{secret:?}");
assert!(!debug.contains("supersecret"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_secret_string_clone() {
let secret = SecretString::new("password");
let cloned = secret.clone();
assert_eq!(cloned.expose_secret(), "password");
}
#[test]
fn test_secure_credentials_sql_server() {
let creds = SecureCredentials::sql_server("user", "password");
assert_eq!(creds.username(), Some("user"));
assert_eq!(creds.password(), Some("password"));
assert!(creds.token().is_none());
}
#[test]
fn test_secure_credentials_azure_token() {
let creds = SecureCredentials::azure_token("my-token");
assert!(creds.username().is_none());
assert!(creds.password().is_none());
assert_eq!(creds.token(), Some("my-token"));
}
#[test]
fn test_secure_credentials_debug_redacts_password() {
let creds = SecureCredentials::sql_server("user", "supersecret");
let debug = format!("{creds:?}");
assert!(debug.contains("user"));
assert!(!debug.contains("supersecret"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_secure_credentials_debug_redacts_token() {
let creds = SecureCredentials::azure_token("supersecrettoken");
let debug = format!("{creds:?}");
assert!(!debug.contains("supersecrettoken"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn test_secure_credentials_from_credentials() {
let creds = Credentials::sql_server("user", "password");
let secure: SecureCredentials = creds.into();
assert_eq!(secure.username(), Some("user"));
assert_eq!(secure.password(), Some("password"));
}
#[test]
fn test_secure_credentials_clone() {
let creds = SecureCredentials::sql_server("user", "password");
let cloned = creds.clone();
assert_eq!(cloned.username(), Some("user"));
assert_eq!(cloned.password(), Some("password"));
}
}
}