Skip to main content

passay_rs/rule/
history.rs

1use crate::rule::reference::{Reference, Salt};
2use crate::rule::rule_result::RuleResult;
3use crate::rule::{PasswordData, Rule};
4use std::any::Any;
5use std::collections::HashMap;
6use std::fmt::{Debug, Formatter};
7
8pub const ERROR_CODE: &str = "HISTORY_VIOLATION";
9
10/// Rule for determining if a password matches one of any previous password a user has chosen. If no historical password
11/// reference has been set, then passwords will meet this rule. See also [PasswordData::password_references]
12///
13/// # Example
14///
15/// ```
16///  use passay_rs::rule::history::HistoryRule;
17///  use passay_rs::rule::history::HistoricalReference;
18///  use passay_rs::rule::PasswordData;
19///  use passay_rs::rule::reference::Reference;
20///  use passay_rs::rule::Rule;
21///
22///  let rule = HistoryRule::default();
23///  let history: Vec<Box<dyn Reference>> =
24///      vec![Box::new(HistoricalReference::with_password_label(
25///          "t3stUs3r03".to_string(),
26///          "history".to_string(),
27///      ))];
28///  let password = PasswordData::new(
29///      "t3stUs3r03".to_string(),
30///      Some("testuser".to_string()),
31///      history,
32///  );
33///  let result = rule.validate(&password);
34///  assert!(!result.valid());
35/// ```
36#[derive(Clone)]
37pub struct HistoryRule {
38    report_all: bool,
39}
40
41impl HistoryRule {
42    pub fn new(report_all: bool) -> HistoryRule {
43        HistoryRule { report_all }
44    }
45}
46
47pub fn create_rule_result_detail_parameters(len: usize) -> HashMap<String, String> {
48    let mut map = HashMap::with_capacity(1);
49    map.insert("historySize".to_string(), len.to_string());
50    map
51}
52impl Rule for HistoryRule {
53    fn validate(&self, password_data: &PasswordData) -> RuleResult {
54        validate_with_history_references(self.report_all, password_data, matches)
55    }
56}
57
58pub(super) fn validate_with_history_references<F: Fn(&str, &HistoricalReference) -> bool>(
59    report_all: bool,
60    password_data: &PasswordData,
61    matcher: F,
62) -> RuleResult {
63    let mut result = RuleResult::default();
64
65    let mut len = 0;
66    for rf in password_data.password_references() {
67        if rf.as_any().downcast_ref::<HistoricalReference>().is_some() {
68            len += 1;
69        }
70    }
71
72    for rf in password_data.password_references() {
73        if let Some(rf) = rf.as_any().downcast_ref::<HistoricalReference>() {
74            let cleartext = password_data.password();
75            if matcher(cleartext, rf) {
76                result.add_error(ERROR_CODE, Some(create_rule_result_detail_parameters(len)));
77                if !report_all {
78                    return result;
79                }
80            }
81        }
82    }
83    result
84}
85
86impl Default for HistoryRule {
87    fn default() -> Self {
88        Self { report_all: true }
89    }
90}
91fn matches(password: &str, rf: &HistoricalReference) -> bool {
92    password == rf.password()
93}
94
95pub struct HistoricalReference {
96    label: Option<String>,
97    password: String,
98    salt: Option<Salt>,
99}
100
101impl HistoricalReference {
102    pub fn new(password: String, label: Option<String>, salt: Option<Salt>) -> HistoricalReference {
103        HistoricalReference {
104            password,
105            label,
106            salt,
107        }
108    }
109
110    pub fn with_password(password: String) -> HistoricalReference {
111        Self::new(password, None, None)
112    }
113    pub fn with_password_label(password: String, label: String) -> HistoricalReference {
114        Self::new(password, Some(label), None)
115    }
116}
117
118impl Debug for HistoricalReference {
119    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("HistoricalReference")
121            .field("password", &self.password)
122            .field("label", &self.label)
123            .finish()
124    }
125}
126
127impl Reference for HistoricalReference {
128    fn password(&self) -> &str {
129        self.password.as_str()
130    }
131
132    fn salt(&self) -> &Option<Salt> {
133        &self.salt
134    }
135
136    fn as_any(&self) -> &dyn Any {
137        self
138    }
139}
140
141#[cfg(test)]
142mod test {
143    use crate::rule::PasswordData;
144    use crate::rule::history::{ERROR_CODE, HistoricalReference, HistoryRule};
145    use crate::rule::reference::Reference;
146    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};
147
148    #[test]
149    fn test_passwords() {
150        let rule = HistoryRule::default();
151        let rule_report_first = HistoryRule::new(false);
152        let empty_rule = HistoryRule::default();
153        let test_cases: Vec<RulePasswordTestItem> = vec![
154            RulePasswordTestItem(
155                Box::new(rule.clone()),
156                PasswordData::new(
157                    "t3stUs3r00".to_string(),
158                    Some("testuser".to_string()),
159                    setup_history(),
160                ),
161                vec![],
162            ),
163            RulePasswordTestItem(
164                Box::new(rule.clone()),
165                PasswordData::new(
166                    "t3stUs3r01".to_string(),
167                    Some("testuser".to_string()),
168                    setup_history(),
169                ),
170                vec![ERROR_CODE],
171            ),
172            RulePasswordTestItem(
173                Box::new(rule.clone()),
174                PasswordData::new(
175                    "t3stUs3r02".to_string(),
176                    Some("testuser".to_string()),
177                    setup_history(),
178                ),
179                vec![ERROR_CODE, ERROR_CODE],
180            ),
181            RulePasswordTestItem(
182                Box::new(rule.clone()),
183                PasswordData::new(
184                    "t3stUs3r03".to_string(),
185                    Some("testuser".to_string()),
186                    setup_history(),
187                ),
188                vec![ERROR_CODE],
189            ),
190            RulePasswordTestItem(
191                Box::new(rule_report_first.clone()),
192                PasswordData::new(
193                    "t3stUs3r00".to_string(),
194                    Some("testuser".to_string()),
195                    setup_history(),
196                ),
197                vec![],
198            ),
199            RulePasswordTestItem(
200                Box::new(rule_report_first.clone()),
201                PasswordData::new(
202                    "t3stUs3r01".to_string(),
203                    Some("testuser".to_string()),
204                    setup_history(),
205                ),
206                vec![ERROR_CODE],
207            ),
208            RulePasswordTestItem(
209                Box::new(rule_report_first.clone()),
210                PasswordData::new(
211                    "t3stUs3r02".to_string(),
212                    Some("testuser".to_string()),
213                    setup_history(),
214                ),
215                vec![ERROR_CODE],
216            ),
217            RulePasswordTestItem(
218                Box::new(rule_report_first.clone()),
219                PasswordData::new(
220                    "t3stUs3r03".to_string(),
221                    Some("testuser".to_string()),
222                    setup_history(),
223                ),
224                vec![ERROR_CODE],
225            ),
226            RulePasswordTestItem(
227                Box::new(empty_rule.clone()),
228                PasswordData::with_password_and_user(
229                    "t3stUs3r00".to_string(),
230                    Some("testuser".to_string()),
231                ),
232                vec![],
233            ),
234            RulePasswordTestItem(
235                Box::new(empty_rule.clone()),
236                PasswordData::with_password_and_user(
237                    "t3stUs3r01".to_string(),
238                    Some("testuser".to_string()),
239                ),
240                vec![],
241            ),
242            RulePasswordTestItem(
243                Box::new(empty_rule.clone()),
244                PasswordData::with_password_and_user(
245                    "t3stUs3r02".to_string(),
246                    Some("testuser".to_string()),
247                ),
248                vec![],
249            ),
250            RulePasswordTestItem(
251                Box::new(empty_rule.clone()),
252                PasswordData::with_password_and_user(
253                    "t3stUs3r03".to_string(),
254                    Some("testuser".to_string()),
255                ),
256                vec![],
257            ),
258        ];
259        check_passwords(test_cases);
260    }
261
262    #[test]
263    fn test_messages() {
264        let rule = HistoryRule::default();
265        let rule_report_first = HistoryRule::new(false);
266
267        let history_len = setup_history().len();
268        let message = "HISTORY_VIOLATION,".to_owned() + history_len.to_string().as_str();
269
270        let test_cases: Vec<RulePasswordTestItem> = vec![
271            RulePasswordTestItem(
272                Box::new(rule.clone()),
273                PasswordData::new(
274                    "t3stUs3r01".to_string(),
275                    Some("testuser".to_string()),
276                    setup_history(),
277                ),
278                vec![&message],
279            ),
280            RulePasswordTestItem(
281                Box::new(rule.clone()),
282                PasswordData::new(
283                    "t3stUs3r02".to_string(),
284                    Some("testuser".to_string()),
285                    setup_history(),
286                ),
287                vec![&message, &message],
288            ),
289            RulePasswordTestItem(
290                Box::new(rule_report_first),
291                PasswordData::new(
292                    "t3stUs3r02".to_string(),
293                    Some("testuser".to_string()),
294                    setup_history(),
295                ),
296                vec![&message],
297            ),
298        ];
299        check_messages(test_cases);
300    }
301
302    fn setup_history() -> Vec<Box<dyn Reference>> {
303        vec![
304            Box::new(HistoricalReference::with_password_label(
305                "t3stUs3r01".to_string(),
306                "history".to_string(),
307            )),
308            Box::new(HistoricalReference::with_password_label(
309                "t3stUs3r02".to_string(),
310                "history".to_string(),
311            )),
312            Box::new(HistoricalReference::with_password_label(
313                "t3stUs3r03".to_string(),
314                "history".to_string(),
315            )),
316            Box::new(HistoricalReference::with_password_label(
317                "t3stUs3r02".to_string(),
318                "history".to_string(),
319            )),
320        ]
321    }
322}