"""Configurable password policy (``INFRASYNTH_SECURITY``). Wire it in the consuming project with:: AUTH_PASSWORD_VALIDATORS = [ {"NAME": "infrasynth.security.password_validation.PasswordPolicyValidator"}, ] The knob names are the ones documented in ``INFRASYNTH_SECURITY``: ``PASSWORD_MIN_LENGTH``, ``PASSWORD_REQUIRE_UPPERCASE``, ``PASSWORD_REQUIRE_DIGIT``, ``PASSWORD_REQUIRE_SPECIAL_CHAR``. """ from __future__ import annotations import re from django.core.exceptions import ValidationError from infrasynth.shared.settings_utils import get_setting __all__ = ["PasswordPolicyValidator"] _SPECIAL = re.compile(r"[^A-Za-z0-9]") class PasswordPolicyValidator: def __init__(self) -> None: pass @staticmethod def _config(key: str, default): return get_setting("INFRASYNTH_SECURITY", key, default) def validate(self, password: str, user=None) -> None: errors: list[str] = [] min_length = int(self._config("PASSWORD_MIN_LENGTH", 8)) if len(password) < min_length: errors.append(f"This password must contain at least {min_length} characters.") if self._config("PASSWORD_REQUIRE_UPPERCASE", True) and not any(c.isupper() for c in password): errors.append("This password must contain at least one uppercase letter.") if self._config("PASSWORD_REQUIRE_DIGIT", True) and not any(c.isdigit() for c in password): errors.append("This password must contain at least one digit.") if self._config("PASSWORD_REQUIRE_SPECIAL_CHAR", True) and not _SPECIAL.search(password): errors.append("This password must contain at least one special character.") if errors: raise ValidationError(errors) def get_help_text(self) -> str: min_length = int(self._config("PASSWORD_MIN_LENGTH", 8)) return f"Your password must be at least {min_length} characters and meet the site's strength rules."