Skip to main content

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