infrasynth-backend-kit/infrasynth/security/password_validation.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
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
2026-09-24 10:41:21 -05:00

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."