Skip to main content

tiberius/client/
auth.rs

1use std::fmt::Debug;
2use zeroize::Zeroizing;
3
4#[derive(Clone, PartialEq, Eq)]
5pub struct SqlServerAuth {
6    user: String,
7    password: Zeroizing<String>,
8}
9
10impl SqlServerAuth {
11    pub(crate) fn into_credentials(self) -> (String, Zeroizing<String>) {
12        (self.user, self.password)
13    }
14}
15
16impl Debug for SqlServerAuth {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("SqlServerAuth")
19            .field("user", &self.user)
20            .field("password", &"<HIDDEN>")
21            .finish()
22    }
23}
24
25#[derive(Clone, PartialEq, Eq)]
26#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
27#[cfg_attr(
28    docsrs,
29    doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
30)]
31pub struct WindowsAuth {
32    pub(crate) user: String,
33    pub(crate) password: Zeroizing<String>,
34    pub(crate) domain: Option<String>,
35}
36
37#[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
38#[cfg_attr(
39    docsrs,
40    doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
41)]
42impl Debug for WindowsAuth {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("WindowsAuth")
45            .field("user", &self.user)
46            .field("password", &"<HIDDEN>")
47            .field("domain", &self.domain)
48            .finish()
49    }
50}
51
52/// Defines the method of authentication to the server.
53#[derive(Clone, PartialEq, Eq)]
54pub enum AuthMethod {
55    /// Authenticate directly with SQL Server.
56    SqlServer(SqlServerAuth),
57    /// Authenticate with Windows credentials. On Windows this uses SSPI via the
58    /// `winauth` feature; on Unix it uses NTLM (no Kerberos) via the `sspi-rs`
59    /// feature.
60    #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
61    #[cfg_attr(
62        docsrs,
63        doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
64    )]
65    Windows(WindowsAuth),
66    /// Authenticate as the currently logged in user. On Windows uses SSPI and
67    /// Kerberos on Unix platforms.
68    #[cfg(any(
69        all(windows, feature = "winauth"),
70        all(unix, feature = "integrated-auth-gssapi"),
71        doc
72    ))]
73    #[cfg_attr(
74        docsrs,
75        doc(cfg(any(windows, all(unix, feature = "integrated-auth-gssapi"))))
76    )]
77    Integrated,
78    /// Authenticate with an AAD token. The token should encode an AAD user/service principal
79    /// which has access to SQL Server.
80    AADToken(String),
81    #[doc(hidden)]
82    None,
83}
84
85// Manual Debug so the AAD bearer token is never printed. The credential-bearing
86// SqlServer/Windows variants delegate to their inner types, which already redact.
87impl Debug for AuthMethod {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::SqlServer(a) => f.debug_tuple("SqlServer").field(a).finish(),
91            #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
92            Self::Windows(a) => f.debug_tuple("Windows").field(a).finish(),
93            #[cfg(any(
94                all(windows, feature = "winauth"),
95                all(unix, feature = "integrated-auth-gssapi"),
96                doc
97            ))]
98            Self::Integrated => f.write_str("Integrated"),
99            Self::AADToken(_) => f.debug_tuple("AADToken").field(&"<HIDDEN>").finish(),
100            Self::None => f.write_str("None"),
101        }
102    }
103}
104
105impl AuthMethod {
106    /// Construct a new SQL Server authentication configuration.
107    pub fn sql_server(user: impl ToString, password: impl ToString) -> Self {
108        Self::SqlServer(SqlServerAuth {
109            user: user.to_string(),
110            password: Zeroizing::new(password.to_string()),
111        })
112    }
113
114    /// Construct a new Windows authentication configuration.
115    #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"), doc))]
116    #[cfg_attr(
117        docsrs,
118        doc(cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs"))))
119    )]
120    pub fn windows(user: impl AsRef<str>, password: impl ToString) -> Self {
121        let (domain, user) = match user.as_ref().find('\\') {
122            Some(idx) => (Some(&user.as_ref()[..idx]), &user.as_ref()[idx + 1..]),
123            _ => (None, user.as_ref()),
124        };
125
126        Self::Windows(WindowsAuth {
127            user: user.to_string(),
128            password: Zeroizing::new(password.to_string()),
129            domain: domain.map(|s| s.to_string()),
130        })
131    }
132
133    /// Construct a new configuration with AAD auth token.
134    pub fn aad_token(token: impl ToString) -> Self {
135        Self::AADToken(token.to_string())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::AuthMethod;
142    use zeroize::Zeroize;
143
144    #[test]
145    fn sql_server_password_can_be_consumed_and_zeroized() {
146        let AuthMethod::SqlServer(auth) = AuthMethod::sql_server("sa", "secret") else {
147            unreachable!();
148        };
149
150        let (user, mut password) = auth.into_credentials();
151
152        assert_eq!("sa", user);
153        assert_eq!("secret", password.as_str());
154
155        password.zeroize();
156
157        assert!(password.is_empty());
158    }
159
160    #[test]
161    fn debug_redacts_credentials() {
162        let sql = format!("{:?}", AuthMethod::sql_server("sa", "sql-secret"));
163        assert!(!sql.contains("sql-secret"), "SQL password leaked: {sql}");
164
165        let aad = format!("{:?}", AuthMethod::aad_token("aad-secret-token"));
166        assert!(!aad.contains("aad-secret-token"), "AAD token leaked: {aad}");
167        assert!(aad.contains("HIDDEN"));
168    }
169
170    #[test]
171    fn debug_none_variant() {
172        assert_eq!(format!("{:?}", AuthMethod::None), "None");
173    }
174
175    #[cfg(any(all(windows, feature = "winauth"), all(unix, feature = "sspi-rs")))]
176    #[test]
177    fn windows_auth_parses_domain_and_debug_redacts() {
178        // `DOMAIN\user` form exercises the domain-splitting branch of `windows()`.
179        let auth = AuthMethod::windows("DOMAIN\\user", "win-secret");
180        let dbg = format!("{:?}", auth);
181        assert!(dbg.contains("Windows"), "variant name missing: {dbg}");
182        assert!(dbg.contains("DOMAIN"), "domain not preserved: {dbg}");
183        assert!(dbg.contains("user"), "user not preserved: {dbg}");
184        assert!(!dbg.contains("win-secret"), "password leaked: {dbg}");
185
186        // No backslash exercises the domain-less branch.
187        let plain = AuthMethod::windows("plainuser", "pw");
188        let dbg = format!("{:?}", plain);
189        assert!(dbg.contains("plainuser"), "user not preserved: {dbg}");
190        assert!(dbg.contains("None"), "domain should be None: {dbg}");
191    }
192
193    #[cfg(any(
194        all(windows, feature = "winauth"),
195        all(unix, feature = "integrated-auth-gssapi")
196    ))]
197    #[test]
198    fn integrated_debug() {
199        assert_eq!(format!("{:?}", AuthMethod::Integrated), "Integrated");
200    }
201}