use secrecy::{ExposeSecret, SecretString};
use zeroize::Zeroize;
pub(crate) fn secret_from_string(mut s: String) -> SecretString {
let boxed: Box<str> = s.as_str().into();
s.zeroize();
SecretString::new(boxed)
}
#[derive(Clone, Debug)]
pub struct SqlServerAuth {
user: String,
password: SecretString,
}
impl SqlServerAuth {
pub(crate) fn into_credentials(self) -> (String, SecretString) {
(self.user, self.password)
}
}
impl PartialEq for SqlServerAuth {
fn eq(&self, other: &Self) -> bool {
self.user == other.user && self.password.expose_secret() == other.password.expose_secret()
}
}
impl Eq for SqlServerAuth {}
#[derive(Clone, Debug)]
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
#[cfg_attr(
docsrs,
doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
)]
pub struct WindowsAuth {
pub(crate) user: String,
pub(crate) password: SecretString,
pub(crate) domain: Option<String>,
}
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
impl PartialEq for WindowsAuth {
fn eq(&self, other: &Self) -> bool {
self.user == other.user
&& self.domain == other.domain
&& self.password.expose_secret() == other.password.expose_secret()
}
}
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
impl Eq for WindowsAuth {}
#[derive(Clone, Debug)]
pub enum AuthMethod {
SqlServer(SqlServerAuth),
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
#[cfg_attr(
docsrs,
doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
)]
Windows(WindowsAuth),
#[cfg(any(
all(windows, feature = "winauth"),
all(unix, feature = "integrated-auth-gssapi"),
doc
))]
#[cfg_attr(
docsrs,
doc(cfg(any(windows, all(unix, feature = "integrated-auth-gssapi"))))
)]
Integrated,
AADToken(SecretString),
#[doc(hidden)]
None,
}
impl PartialEq for AuthMethod {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::SqlServer(a), Self::SqlServer(b)) => a == b,
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
(Self::Windows(a), Self::Windows(b)) => a == b,
#[cfg(any(
all(windows, feature = "winauth"),
all(unix, feature = "integrated-auth-gssapi"),
doc
))]
(Self::Integrated, Self::Integrated) => true,
(Self::AADToken(a), Self::AADToken(b)) => a.expose_secret() == b.expose_secret(),
(Self::None, Self::None) => true,
_ => false,
}
}
}
impl Eq for AuthMethod {}
impl AuthMethod {
pub fn sql_server(user: impl ToString, password: impl ToString) -> Self {
Self::SqlServer(SqlServerAuth {
user: user.to_string(),
password: secret_from_string(password.to_string()),
})
}
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
#[cfg_attr(
docsrs,
doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
)]
pub fn windows(user: impl AsRef<str>, password: impl ToString) -> Self {
let (domain, user) = match user.as_ref().find('\\') {
Some(idx) => (Some(&user.as_ref()[..idx]), &user.as_ref()[idx + 1..]),
_ => (None, user.as_ref()),
};
Self::Windows(WindowsAuth {
user: user.to_string(),
password: secret_from_string(password.to_string()),
domain: domain.map(|s| s.to_string()),
})
}
pub fn aad_token(token: impl ToString) -> Self {
Self::AADToken(secret_from_string(token.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::AuthMethod;
use secrecy::ExposeSecret;
#[test]
fn stored_credentials_are_zeroize_on_drop() {
fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<secrecy::SecretString>();
}
#[test]
fn sql_server_password_can_be_consumed_and_exposed() {
let AuthMethod::SqlServer(auth) = AuthMethod::sql_server("sa", "secret") else {
unreachable!();
};
let (user, password) = auth.into_credentials();
assert_eq!("sa", user);
assert_eq!("secret", password.expose_secret());
}
#[test]
fn aad_token_exposes_the_right_value() {
let AuthMethod::AADToken(token) = AuthMethod::aad_token("aad-secret-token") else {
unreachable!();
};
assert_eq!("aad-secret-token", token.expose_secret());
}
#[test]
fn debug_redacts_credentials() {
let sql = format!("{:?}", AuthMethod::sql_server("sa", "sql-secret"));
assert!(!sql.contains("sql-secret"), "SQL password leaked: {sql}");
assert!(sql.contains("sa"), "user should be shown: {sql}");
assert!(sql.contains("REDACTED"), "password not redacted: {sql}");
let aad = format!("{:?}", AuthMethod::aad_token("aad-secret-token"));
assert!(!aad.contains("aad-secret-token"), "AAD token leaked: {aad}");
assert!(aad.contains("REDACTED"), "AAD token not redacted: {aad}");
}
#[test]
fn secret_from_string_preserves_value_with_spare_capacity() {
let mut s = String::with_capacity(64);
s.push_str("pw");
assert_eq!(super::secret_from_string(s).expose_secret(), "pw");
}
#[test]
fn debug_none_variant() {
assert_eq!(format!("{:?}", AuthMethod::None), "None");
}
#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs")))]
#[test]
fn windows_auth_parses_domain_and_debug_redacts() {
let auth = AuthMethod::windows("DOMAIN\\user", "win-secret");
let dbg = format!("{:?}", auth);
assert!(dbg.contains("Windows"), "variant name missing: {dbg}");
assert!(dbg.contains("DOMAIN"), "domain not preserved: {dbg}");
assert!(dbg.contains("user"), "user not preserved: {dbg}");
assert!(!dbg.contains("win-secret"), "password leaked: {dbg}");
assert!(dbg.contains("REDACTED"), "password not redacted: {dbg}");
let plain = AuthMethod::windows("plainuser", "pw");
let dbg = format!("{:?}", plain);
assert!(dbg.contains("plainuser"), "user not preserved: {dbg}");
assert!(dbg.contains("None"), "domain should be None: {dbg}");
}
#[cfg(any(
all(windows, feature = "winauth"),
all(unix, feature = "integrated-auth-gssapi")
))]
#[test]
fn integrated_debug() {
assert_eq!(format!("{:?}", AuthMethod::Integrated), "Integrated");
}
}