1use std::borrow::Cow;
7
8use crate::credentials::Credentials;
9use crate::error::AuthError;
10use crate::provider::{AuthData, AuthMethod, AuthProvider};
11
12#[derive(Clone)]
30pub struct SqlServerAuth {
31 username: Cow<'static, str>,
32 password: Cow<'static, str>,
33}
34
35impl SqlServerAuth {
36 pub fn new(
38 username: impl Into<Cow<'static, str>>,
39 password: impl Into<Cow<'static, str>>,
40 ) -> Self {
41 Self {
42 username: username.into(),
43 password: password.into(),
44 }
45 }
46
47 pub fn from_credentials(credentials: &Credentials) -> Result<Self, AuthError> {
51 match credentials {
52 Credentials::SqlServer { username, password } => Ok(Self {
53 username: Cow::Owned(username.to_string()),
54 password: Cow::Owned(password.to_string()),
55 }),
56 _ => Err(AuthError::UnsupportedMethod(
57 "SqlServerAuth requires SQL Server credentials".into(),
58 )),
59 }
60 }
61
62 #[must_use]
64 pub fn username(&self) -> &str {
65 &self.username
66 }
67
68 #[must_use]
80 pub fn encode_password(password: &str) -> Vec<u8> {
81 password
82 .encode_utf16()
83 .flat_map(|c| {
84 let byte1 = (c & 0xFF) as u8;
85 let byte2 = (c >> 8) as u8;
86
87 let encoded1 = (byte1 ^ 0xA5).rotate_right(4);
89 let encoded2 = (byte2 ^ 0xA5).rotate_right(4);
90
91 [encoded1, encoded2]
92 })
93 .collect()
94 }
95}
96
97impl AuthProvider for SqlServerAuth {
98 fn method(&self) -> AuthMethod {
99 AuthMethod::SqlServer
100 }
101
102 fn authenticate(&self) -> Result<AuthData, AuthError> {
103 tracing::debug!(
104 username = %self.username,
105 "authenticating with SQL Server credentials"
106 );
107
108 let password_bytes = Self::encode_password(&self.password);
109
110 Ok(AuthData::SqlServer {
111 username: self.username.to_string(),
112 password_bytes,
113 })
114 }
115}
116
117impl std::fmt::Debug for SqlServerAuth {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("SqlServerAuth")
120 .field("username", &self.username)
121 .field("password", &"[REDACTED]")
122 .finish()
123 }
124}
125
126#[deprecated(since = "0.2.0", note = "Use SqlServerAuth instead")]
131pub struct SqlAuthenticator;
132
133#[allow(deprecated)]
134impl SqlAuthenticator {
135 #[must_use]
137 pub fn new() -> Self {
138 Self
139 }
140
141 #[must_use]
143 pub fn encode_password(password: &str) -> Vec<u8> {
144 SqlServerAuth::encode_password(password)
145 }
146}
147
148#[allow(deprecated)]
149impl Default for SqlAuthenticator {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used, clippy::panic)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_password_encoding() {
162 let encoded = SqlServerAuth::encode_password("test");
164 assert!(!encoded.is_empty());
165 assert_eq!(encoded.len(), 8); }
167
168 #[test]
169 fn test_password_encoding_known_value() {
170 let encoded = SqlServerAuth::encode_password("a");
175 assert_eq!(encoded, vec![0x4C, 0x5A]);
176 }
177
178 #[test]
179 fn test_sql_server_auth_provider() {
180 let auth = SqlServerAuth::new("sa", "Password123!");
181
182 assert_eq!(auth.method(), AuthMethod::SqlServer);
183 assert_eq!(auth.username(), "sa");
184
185 let data = auth.authenticate().unwrap();
186 match data {
187 AuthData::SqlServer {
188 username,
189 password_bytes,
190 } => {
191 assert_eq!(username, "sa");
192 assert!(!password_bytes.is_empty());
193 }
194 _ => panic!("Expected SqlServer auth data"),
195 }
196 }
197
198 #[test]
199 fn test_from_credentials() {
200 let creds = Credentials::sql_server("user", "pass");
201 let auth = SqlServerAuth::from_credentials(&creds).unwrap();
202 assert_eq!(auth.username(), "user");
203 }
204
205 #[test]
206 fn test_from_credentials_wrong_type() {
207 let creds = Credentials::azure_token("token");
208 let result = SqlServerAuth::from_credentials(&creds);
209 assert!(result.is_err());
210 }
211
212 #[test]
213 fn test_debug_redacts_password() {
214 let auth = SqlServerAuth::new("sa", "secret");
215 let debug = format!("{:?}", auth);
216 assert!(debug.contains("sa"));
217 assert!(!debug.contains("secret"));
218 assert!(debug.contains("[REDACTED]"));
219 }
220}