1use std::fmt;
7
8use secrecy::{ExposeSecret, SecretString};
9
10use crate::Error;
11
12pub(crate) enum AuthenticationCredentials {
14 ApiKey(Credentials),
16 Application(ApplicationCredentials),
18}
19
20impl fmt::Debug for AuthenticationCredentials {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::ApiKey(credentials) => credentials.fmt(f),
24 Self::Application(credentials) => credentials.fmt(f),
25 }
26 }
27}
28
29pub struct Credentials {
33 user_name: SecretString,
34 api_key: SecretString,
35}
36
37impl Credentials {
38 pub fn new(user_name: impl Into<String>, api_key: impl Into<String>) -> Result<Self, Error> {
45 let user_name = user_name.into();
46 let api_key = api_key.into();
47 validate_secret("username", &user_name)?;
48 validate_secret("API key", &api_key)?;
49 Ok(Self {
50 user_name: SecretString::from(user_name),
51 api_key: SecretString::from(api_key),
52 })
53 }
54
55 pub(crate) fn expose_user_name(&self) -> &str {
56 self.user_name.expose_secret()
57 }
58
59 pub(crate) fn expose_api_key(&self) -> &str {
60 self.api_key.expose_secret()
61 }
62}
63
64impl fmt::Debug for Credentials {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("Credentials")
67 .field("user_name", &"[REDACTED]")
68 .field("api_key", &"[REDACTED]")
69 .finish()
70 }
71}
72
73pub struct ApplicationCredentials {
79 user_name: SecretString,
80 password: SecretString,
81 device_id: SecretString,
82 app_id: SecretString,
83 verify_key: SecretString,
84}
85
86impl ApplicationCredentials {
87 pub fn builder(
89 user_name: impl Into<String>,
90 password: impl Into<String>,
91 ) -> ApplicationCredentialsBuilder {
92 ApplicationCredentialsBuilder {
93 user_name: SecretString::from(user_name.into()),
94 password: SecretString::from(password.into()),
95 device_id: None,
96 app_id: None,
97 verify_key: None,
98 }
99 }
100
101 pub(crate) fn expose_user_name(&self) -> &str {
102 self.user_name.expose_secret()
103 }
104
105 pub(crate) fn expose_password(&self) -> &str {
106 self.password.expose_secret()
107 }
108
109 pub(crate) fn expose_device_id(&self) -> &str {
110 self.device_id.expose_secret()
111 }
112
113 pub(crate) fn expose_app_id(&self) -> &str {
114 self.app_id.expose_secret()
115 }
116
117 pub(crate) fn expose_verify_key(&self) -> &str {
118 self.verify_key.expose_secret()
119 }
120}
121
122impl fmt::Debug for ApplicationCredentials {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.debug_struct("ApplicationCredentials")
125 .field("user_name", &"[REDACTED]")
126 .field("password", &"[REDACTED]")
127 .field("device_id", &"[REDACTED]")
128 .field("app_id", &"[REDACTED]")
129 .field("verify_key", &"[REDACTED]")
130 .finish()
131 }
132}
133
134#[must_use = "an ApplicationCredentialsBuilder does nothing until build is called"]
136pub struct ApplicationCredentialsBuilder {
137 user_name: SecretString,
138 password: SecretString,
139 device_id: Option<SecretString>,
140 app_id: Option<SecretString>,
141 verify_key: Option<SecretString>,
142}
143
144impl ApplicationCredentialsBuilder {
145 pub fn device_id(mut self, device_id: impl Into<String>) -> Self {
147 self.device_id = Some(SecretString::from(device_id.into()));
148 self
149 }
150
151 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
153 self.app_id = Some(SecretString::from(app_id.into()));
154 self
155 }
156
157 pub fn verify_key(mut self, verify_key: impl Into<String>) -> Self {
159 self.verify_key = Some(SecretString::from(verify_key.into()));
160 self
161 }
162
163 pub fn build(self) -> Result<ApplicationCredentials, Error> {
170 let device_id = self
171 .device_id
172 .ok_or_else(|| Error::Configuration("application device ID is required".to_owned()))?;
173 let app_id = self
174 .app_id
175 .ok_or_else(|| Error::Configuration("application identifier is required".to_owned()))?;
176 let verify_key = self.verify_key.ok_or_else(|| {
177 Error::Configuration("application verification key is required".to_owned())
178 })?;
179 validate_secret("username", self.user_name.expose_secret())?;
180 validate_secret("password", self.password.expose_secret())?;
181 validate_secret("device ID", device_id.expose_secret())?;
182 validate_secret("application identifier", app_id.expose_secret())?;
183 validate_secret("verification key", verify_key.expose_secret())?;
184 Ok(ApplicationCredentials {
185 user_name: self.user_name,
186 password: self.password,
187 device_id,
188 app_id,
189 verify_key,
190 })
191 }
192}
193
194impl fmt::Debug for ApplicationCredentialsBuilder {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 f.debug_struct("ApplicationCredentialsBuilder")
197 .field("user_name", &"[REDACTED]")
198 .field("password", &"[REDACTED]")
199 .field("device_id", &self.device_id.as_ref().map(|_| "[REDACTED]"))
200 .field("app_id", &self.app_id.as_ref().map(|_| "[REDACTED]"))
201 .field(
202 "verify_key",
203 &self.verify_key.as_ref().map(|_| "[REDACTED]"),
204 )
205 .finish()
206 }
207}
208
209fn validate_secret(name: &str, value: &str) -> Result<(), Error> {
210 if value.is_empty() || value.trim() != value {
211 return Err(Error::Configuration(format!(
212 "{name} must be non-empty and must not contain surrounding whitespace"
213 )));
214 }
215 Ok(())
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 const USER_NAME: &str = "synthetic-user";
223 const PASSWORD: &str = "synthetic-password";
224 const DEVICE_ID: &str = "synthetic-device";
225 const APP_ID: &str = "synthetic-app";
226 const VERIFY_KEY: &str = "synthetic-verify-key";
227
228 fn build_application_credentials(
229 user_name: &str,
230 password: &str,
231 device_id: &str,
232 app_id: &str,
233 verify_key: &str,
234 ) -> Result<ApplicationCredentials, Error> {
235 ApplicationCredentials::builder(user_name, password)
236 .device_id(device_id)
237 .app_id(app_id)
238 .verify_key(verify_key)
239 .build()
240 }
241
242 #[test]
243 fn validate_secret_rejects_empty_and_padded_values() {
244 assert!(matches!(
245 validate_secret("fixture", ""),
246 Err(Error::Configuration(_))
247 ));
248 assert!(matches!(
249 validate_secret("fixture", " padded "),
250 Err(Error::Configuration(_))
251 ));
252 assert!(validate_secret("fixture", "valid-value").is_ok());
253 }
254
255 #[test]
256 fn application_credentials_require_device_application_and_verification_fields() {
257 assert!(matches!(
258 ApplicationCredentials::builder(USER_NAME, PASSWORD).build(),
259 Err(Error::Configuration(_))
260 ));
261 assert!(matches!(
262 ApplicationCredentials::builder(USER_NAME, PASSWORD)
263 .device_id(DEVICE_ID)
264 .build(),
265 Err(Error::Configuration(_))
266 ));
267 assert!(matches!(
268 ApplicationCredentials::builder(USER_NAME, PASSWORD)
269 .device_id(DEVICE_ID)
270 .app_id(APP_ID)
271 .build(),
272 Err(Error::Configuration(_))
273 ));
274 }
275
276 #[test]
277 fn application_credentials_reject_empty_or_padded_values() {
278 for result in [
279 build_application_credentials("", PASSWORD, DEVICE_ID, APP_ID, VERIFY_KEY),
280 build_application_credentials(" padded ", PASSWORD, DEVICE_ID, APP_ID, VERIFY_KEY),
281 build_application_credentials(USER_NAME, "", DEVICE_ID, APP_ID, VERIFY_KEY),
282 build_application_credentials(USER_NAME, " padded ", DEVICE_ID, APP_ID, VERIFY_KEY),
283 build_application_credentials(USER_NAME, PASSWORD, "", APP_ID, VERIFY_KEY),
284 build_application_credentials(USER_NAME, PASSWORD, " padded ", APP_ID, VERIFY_KEY),
285 build_application_credentials(USER_NAME, PASSWORD, DEVICE_ID, "", VERIFY_KEY),
286 build_application_credentials(USER_NAME, PASSWORD, DEVICE_ID, " padded ", VERIFY_KEY),
287 build_application_credentials(USER_NAME, PASSWORD, DEVICE_ID, APP_ID, ""),
288 build_application_credentials(USER_NAME, PASSWORD, DEVICE_ID, APP_ID, " padded "),
289 ] {
290 assert!(matches!(result, Err(Error::Configuration(_))));
291 }
292 }
293
294 #[test]
295 fn application_credentials_build_with_valid_values() {
296 assert!(
297 build_application_credentials(USER_NAME, PASSWORD, DEVICE_ID, APP_ID, VERIFY_KEY)
298 .is_ok()
299 );
300 }
301}