passay-rs 0.1.0

A password validation library inspired by the Java Passay library.
Documentation
use crate::rule::reference::{Reference, Salt};
use crate::rule::rule_result::RuleResult;
use crate::rule::{PasswordData, Rule};
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};

pub(super) const ERROR_CODE: &str = "SOURCE_VIOLATION";

/// Rule for determining if a password matches a password from a different source. Useful for when separate systems
/// cannot have matching passwords. If no source password reference has been set, then passwords will meet this rule.
/// See also [PasswordData::password_references]
///
/// # Example
///
/// ```
///  use passay_rs::rule::source::SourceRule;
///  use passay_rs::rule::source::SourceReference;
///  use passay_rs::rule::PasswordData;
///  use passay_rs::rule::reference::Reference;
///  use passay_rs::rule::Rule;
///
///  let rule = SourceRule::default();
///
///  let source: Vec<Box<dyn Reference>> =
///      vec![Box::new( SourceReference::with_password_label(
///                 "t3stUs3r03".to_string(),
///                 "System A".to_string(),
///             ) )];
///  let password = PasswordData::new(
///      "t3stUs3r03".to_string(),
///      Some("testuser".to_string()),
///      source,
///  );
///  let result = rule.validate(&password);
///  assert!(!result.valid());
/// ```
#[derive(Clone)]
pub struct SourceRule {
    report_all: bool,
}

impl SourceRule {
    pub fn new(report_all: bool) -> SourceRule {
        SourceRule { report_all }
    }
}

impl Default for SourceRule {
    fn default() -> Self {
        SourceRule::new(true)
    }
}

impl Rule for SourceRule {
    fn validate(&self, password_data: &PasswordData) -> RuleResult {
        validate_with_source_references(self.report_all, password_data, matches)
    }
}

pub(super) fn validate_with_source_references<F: Fn(&str, &SourceReference) -> bool>(
    report_all: bool,
    password_data: &PasswordData,
    matcher: F,
) -> RuleResult {
    let mut result = RuleResult::default();

    for rf in password_data.password_references() {
        if let Some(rf) = rf.as_any().downcast_ref::<SourceReference>() {
            let cleartext = password_data.password();
            if matcher(cleartext, rf) {
                result.add_error(
                    ERROR_CODE,
                    Some(create_rule_result_detail_parameters(rf.label())),
                );
                if !report_all {
                    return result;
                };
            }
        }
    }

    result
}
fn create_rule_result_detail_parameters(source: &str) -> HashMap<String, String> {
    let mut map = HashMap::with_capacity(1);
    map.insert("source".to_string(), source.to_string());
    map
}
fn matches(password: &str, rf: &SourceReference) -> bool {
    password == rf.password()
}
pub struct SourceReference {
    label: String,
    password: String,
    salt: Option<Salt>,
}

impl SourceReference {
    pub fn new(label: String, password: String, salt: Salt) -> Self {
        SourceReference {
            label,
            password,
            salt: Some(salt),
        }
    }
    pub fn with_password_label(password: String, label: String) -> Self {
        SourceReference {
            label,
            password,
            salt: None,
        }
    }

    pub fn label(&self) -> &str {
        &self.label
    }
}

impl Debug for SourceReference {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SourceReference")
            .field("password", &self.password)
            .field("label", &self.label)
            .finish()
    }
}

impl Reference for SourceReference {
    fn password(&self) -> &str {
        &self.password
    }

    fn salt(&self) -> &Option<Salt> {
        &self.salt
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[cfg(test)]
mod test {
    use crate::rule::PasswordData;
    use crate::rule::reference::Reference;
    use crate::rule::source::{ERROR_CODE, SourceReference, SourceRule};
    use crate::test::{RulePasswordTestItem, check_messages, check_passwords};

    #[test]
    fn test_passwords() {
        let rule = SourceRule::default();
        let rule_report_first = SourceRule::new(false);
        let empty_rule = SourceRule::default();
        let test_cases: Vec<RulePasswordTestItem> = vec![
            RulePasswordTestItem(
                Box::new(rule.clone()),
                PasswordData::new(
                    "t3stUs3r01".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![],
            ),
            RulePasswordTestItem(
                Box::new(rule.clone()),
                PasswordData::new(
                    "t3stUs3r04".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![ERROR_CODE],
            ),
            RulePasswordTestItem(
                Box::new(rule.clone()),
                PasswordData::new(
                    "t3stUs3r05".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![ERROR_CODE, ERROR_CODE],
            ),
            RulePasswordTestItem(
                Box::new(rule_report_first.clone()),
                PasswordData::new(
                    "t3stUs3r01".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![],
            ),
            RulePasswordTestItem(
                Box::new(rule_report_first.clone()),
                PasswordData::new(
                    "t3stUs3r04".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![ERROR_CODE],
            ),
            RulePasswordTestItem(
                Box::new(rule_report_first.clone()),
                PasswordData::new(
                    "t3stUs3r05".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec![ERROR_CODE],
            ),
            RulePasswordTestItem(
                Box::new(empty_rule.clone()),
                PasswordData::with_password_and_user(
                    "t3stUs3r01".to_string(),
                    Some("testuser".to_string()),
                ),
                vec![],
            ),
            RulePasswordTestItem(
                Box::new(empty_rule.clone()),
                PasswordData::with_password_and_user(
                    "t3stUs3r04".to_string(),
                    Some("testuser".to_string()),
                ),
                vec![],
            ),
            RulePasswordTestItem(
                Box::new(empty_rule.clone()),
                PasswordData::with_password_and_user(
                    "t3stUs3r05".to_string(),
                    Some("testuser".to_string()),
                ),
                vec![],
            ),
        ];
        check_passwords(test_cases);
    }
    #[test]
    fn test_messages() {
        let rule = SourceRule::default();
        let rule_report_first = SourceRule::new(false);

        let test_cases: Vec<RulePasswordTestItem> = vec![
            RulePasswordTestItem(
                Box::new(rule.clone()),
                PasswordData::new(
                    "t3stUs3r04".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec!["SOURCE_VIOLATION,System A"],
            ),
            RulePasswordTestItem(
                Box::new(rule.clone()),
                PasswordData::new(
                    "t3stUs3r05".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec!["SOURCE_VIOLATION,System A", "SOURCE_VIOLATION,System A"],
            ),
            RulePasswordTestItem(
                Box::new(rule_report_first),
                PasswordData::new(
                    "t3stUs3r05".to_string(),
                    Some("testuser".to_string()),
                    create_sources(),
                ),
                vec!["SOURCE_VIOLATION,System A"],
            ),
        ];
        check_messages(test_cases);
    }

    fn create_sources() -> Vec<Box<dyn Reference>> {
        vec![
            Box::new(SourceReference::with_password_label(
                "t3stUs3r04".to_string(),
                "System A".to_string(),
            )),
            Box::new(SourceReference::with_password_label(
                "t3stUs3r05".to_string(),
                "System A".to_string(),
            )),
            Box::new(SourceReference::with_password_label(
                "t3stUs3r05".to_string(),
                "System A".to_string(),
            )),
        ]
    }
}