1use derive_builder::Builder;
7use lazy_static::initialize;
8use validator::Validate;
9
10use crate::errors::*;
11use crate::filters::{beep, filter, forbidden};
12use crate::filters::{
13 blacklist::RE_BLACKLIST, profainity::RE_PROFAINITY, user_case_mapped::RE_USERNAME_CASE_MAPPED,
14};
15
16#[derive(Clone, Builder)]
18pub struct Config {
19 #[builder(default = "false")]
21 profanity: bool,
22 #[builder(default = "true")]
24 blacklist: bool,
25 #[builder(default = "true")]
27 username_case_mapped: bool,
28 #[builder(default = "PasswordPolicyBuilder::default().build().unwrap()")]
30 password_policy: PasswordPolicy,
31}
32
33impl PasswordPolicyBuilder {
34 fn validate(&self) -> Result<(), String> {
35 if self.min > self.max {
36 Err("Configuration error: Password max length shorter than min length".to_string())
37 } else {
38 Ok(())
39 }
40 }
41}
42
43#[derive(Clone, Builder)]
44#[builder(build_fn(validate = "Self::validate"))]
45pub struct PasswordPolicy {
46 #[builder(default = "argon2::Config::default()")]
48 argon2: argon2::Config<'static>,
49 #[builder(default = "8")]
51 min: usize,
52 #[builder(default = "64")]
54 max: usize,
55 #[builder(default = "32")]
57 salt_length: usize,
58}
59
60impl Default for PasswordPolicy {
61 fn default() -> Self {
62 PasswordPolicyBuilder::default().build().unwrap()
63 }
64}
65
66#[derive(Validate)]
67struct Email<'a> {
68 #[validate(email)]
69 pub email: &'a str,
70}
71
72impl Default for Config {
73 fn default() -> Self {
74 ConfigBuilder::default().build().unwrap()
75 }
76}
77
78impl Config {
79 pub fn username(&self, username: &str) -> CredsResult<String> {
81 use ammonia::clean;
82 use unicode_normalization::UnicodeNormalization;
83
84 let clean_username = clean(username)
85 .to_lowercase()
86 .nfc()
87 .collect::<String>()
88 .trim()
89 .to_owned();
90
91 self.validate_username(&clean_username)?;
92 Ok(clean_username)
93 }
94
95 pub fn email(&self, email: &str) -> CredsResult<()> {
97 let email = Email {
98 email: email.trim(),
99 };
100 Ok(email.validate()?)
101 }
102
103 fn validate_username(&self, username: &str) -> CredsResult<()> {
104 if self.username_case_mapped {
105 filter(&username)?;
106 }
107 if self.blacklist {
108 forbidden(&username)?;
109 }
110 if self.profanity {
111 beep(&username)?;
112 }
113 Ok(())
114 }
115
116 pub fn password(&self, password: &str) -> CredsResult<String> {
118 use argon2::hash_encoded;
119 use rand::distributions::Alphanumeric;
120 use rand::{thread_rng, Rng};
121
122 let length = password.len();
123
124 if self.password_policy.min > length {
125 return Err(CredsError::PasswordTooShort);
126 }
127
128 if self.password_policy.max < length {
129 return Err(CredsError::PasswordTooLong);
130 }
131
132 let mut rng = thread_rng();
133 let salt: String = std::iter::repeat(())
134 .map(|()| rng.sample(Alphanumeric))
135 .map(char::from)
136 .take(self.password_policy.salt_length)
137 .collect();
138
139 Ok(hash_encoded(
140 password.as_bytes(),
141 salt.as_bytes(),
142 &self.password_policy.argon2,
143 )?)
144 }
145
146 pub fn verify(hash: &str, password: &str) -> CredsResult<bool> {
148 let status = argon2::verify_encoded(hash, password.as_bytes())?;
149 Ok(status)
150 }
151
152 pub fn init(&self) {
158 if self.username_case_mapped {
159 initialize(&RE_USERNAME_CASE_MAPPED);
160 }
161 if self.blacklist {
162 initialize(&RE_BLACKLIST);
163 }
164 if self.profanity {
165 initialize(&RE_PROFAINITY);
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn config_works() {
176 let config = Config::default();
177 assert!(!config.profanity);
178 assert!(config.blacklist);
179 assert!(config.username_case_mapped);
180 assert_eq!(config.password_policy.salt_length, 32);
181
182 let config = ConfigBuilder::default()
183 .username_case_mapped(false)
184 .profanity(true)
185 .blacklist(false)
186 .password_policy(PasswordPolicy::default())
187 .build()
188 .unwrap();
189
190 assert!(config.profanity);
191 assert!(!config.blacklist);
192 assert!(!config.username_case_mapped);
193 }
194
195 #[test]
196 fn creds_email_err() {
197 let config = ConfigBuilder::default()
198 .username_case_mapped(false)
199 .profanity(true)
200 .blacklist(false)
201 .password_policy(PasswordPolicy::default())
202 .build()
203 .unwrap();
204 config.init();
205
206 assert_eq!(config.email("sdfasdf"), Err(CredsError::NotAnEmail));
207 }
208
209 #[test]
210 fn utils_create_new_organisation() {
211 let password = "somepassword";
212 let config = Config::default();
213 config.init();
214
215 config.email("batman@we.net").unwrap();
216 let username = config.username("Realaravinth").unwrap();
217 let hash = config.password(password).unwrap();
218
219 assert_eq!(username, "realaravinth");
220
221 assert!(Config::verify(&hash, password).unwrap(), "verify hashing");
222 }
223
224 #[test]
225 fn username_case_mapped_org() {
226 let config = ConfigBuilder::default()
227 .username_case_mapped(true)
228 .profanity(true)
229 .blacklist(false)
230 .password_policy(PasswordPolicy::default())
231 .build()
232 .unwrap();
233 config.init();
234
235 let username_err = config.username("a@test.com");
236
237 assert_eq!(username_err, Err(CredsError::UsernameCaseMappedError));
238 }
239
240 #[test]
241 fn utils_create_new_profane_organisation() {
242 let config = ConfigBuilder::default()
243 .username_case_mapped(false)
244 .profanity(true)
245 .blacklist(false)
246 .password_policy(PasswordPolicy::default())
247 .build()
248 .unwrap();
249 config.init();
250
251 let username_err = config.username("fuck");
252
253 assert_eq!(username_err, Err(CredsError::ProfainityError));
254 }
255
256 #[test]
257 fn utils_create_new_forbidden_organisation() {
258 let config = Config::default();
259 config.init();
260 let forbidden_err = config.username("webmaster");
261
262 assert_eq!(forbidden_err, Err(CredsError::BlacklistError));
263 }
264
265 #[test]
266 fn password_length_check() {
267 let min_max_error = PasswordPolicyBuilder::default().min(50).max(10).build();
268
269 assert!(min_max_error.is_err());
270
271 let config = ConfigBuilder::default()
272 .password_policy(
273 PasswordPolicyBuilder::default()
274 .min(5)
275 .max(10)
276 .build()
277 .unwrap(),
278 )
279 .build()
280 .unwrap();
281 config.init();
282
283 let too_short_err = config.password("a");
284 let too_long_err = config.password("asdfasdfasdf");
285
286 assert_eq!(too_short_err, Err(CredsError::PasswordTooShort));
287 assert_eq!(too_long_err, Err(CredsError::PasswordTooLong));
288 }
289}