Close the gaps between the documented contract (API-STANDARD, TENANCY, ENTITLEMENTS) and the implementation, and remove committed build artifacts. Security: - verify + process inbound webhooks (HMAC/handler verify, size limit, timestamp tolerance, idempotency via InboundEvent.external_id) - real 2FA login flow (pre-auth challenge; tokens only after verify/recovery) - wire HybridPermission into security/audit views; add API-key rotate and users/<id>/permissions|roles endpoints - tenant-scoped throttling on by default; webhook replay protection - verify MercadoPago webhook signatures - login brute-force guard, configurable password policy, real ALTCHA PoW Correctness: - apply verified billing webhooks idempotently (subscription/entitlement/ invoice/PaymentTransaction); scheduled payment lifecycle jobs - capture audit update diffs automatically; add audit retention purge - working notification retries, per-channel rate limits, log retention - pluggable virus scanner, upload-size limit, pipeline toggle - feature rollout %/environment targeting; settings-driven registrations - workflow guards (instance cap, route depth, self-assignment, clone on re-entry) - wire every previously-dead INFRASYNTH_* setting; drop truly dead ones Delivery: - README + CHANGELOG; CI format check + coverage gate - keep test media out of the tree; untrack .coverage, __pycache__, egg-info, docs/ and invoice artifacts
51 lines
1.9 KiB
Python
51 lines
1.9 KiB
Python
"""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."
|