🔒️(passwords) add validators for production

This enabled various password validators to enforce password complexity.
This commit is contained in:
Quentin BEY
2025-03-28 14:09:01 +01:00
committed by BEY Quentin
parent 838d1267b2
commit dd43483ce6
5 changed files with 153 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added
- 🔒️(passwords) add validators for production #850
- ✨(domains) allow to re-run check on domain if status is failed
- ✨(organization) add `is_active` field
- ✨(domains) notify support when domain status changes #668

View File

@@ -0,0 +1,50 @@
"""Test the production settings for password validation is correct."""
from django.contrib.auth.password_validation import (
get_default_password_validators,
validate_password,
)
import pytest
from core.factories import UserFactory
from people.settings import Production
pytestmark = pytest.mark.django_db
@pytest.fixture(name="use_production_password_validators")
def use_production_password_validators_fixture(settings):
"""Set the production password validators."""
settings.AUTH_PASSWORD_VALIDATORS = Production.AUTH_PASSWORD_VALIDATORS
get_default_password_validators.cache_clear()
assert len(get_default_password_validators()) == 5
yield
get_default_password_validators.cache_clear()
@pytest.mark.parametrize(
"password, error",
[
("password", "This password is too common."),
("password123", "This password is too common."),
("123", "This password is too common."),
("coucou", "This password is too common."),
("john doe 123", "The password is too similar to the name"),
],
)
def test_validate_password_validator(
use_production_password_validators, # pylint: disable=unused-argument
password,
error,
):
"""Test the Mailbox password validation."""
user = UserFactory(name="John Doe")
with pytest.raises(Exception) as excinfo:
validate_password(password, user)
assert error in str(excinfo.value)

View File

@@ -0,0 +1,59 @@
"""Test the production settings for password validation is correct."""
from django.contrib.auth.password_validation import (
get_default_password_validators,
validate_password,
)
import pytest
from mailbox_manager.factories import MailboxFactory
from people.settings import Production
pytestmark = pytest.mark.django_db
@pytest.fixture(name="use_production_password_validators")
def use_production_password_validators_fixture(settings):
"""Set the production password validators."""
settings.AUTH_PASSWORD_VALIDATORS = Production.AUTH_PASSWORD_VALIDATORS
get_default_password_validators.cache_clear()
assert len(get_default_password_validators()) == 5
yield
get_default_password_validators.cache_clear()
@pytest.mark.parametrize(
"password, error",
[
("password", "This password is too common."),
("password123", "This password is too common."),
("123", "This password is too common."),
("coucou", "This password is too common."),
("john doe 123", "The password is too similar to the"),
],
)
def test_validate_password_validator(
use_production_password_validators, # pylint: disable=unused-argument
password,
error,
):
"""Test the Mailbox password validation."""
mailbox_1 = MailboxFactory(
first_name="John",
last_name="Doe",
)
mailbox_2 = MailboxFactory(
local_part="john.doe",
)
with pytest.raises(Exception) as excinfo:
validate_password(password, mailbox_1)
assert error in str(excinfo.value)
with pytest.raises(Exception) as excinfo:
validate_password(password, mailbox_2)
assert error in str(excinfo.value)

View File

@@ -231,6 +231,7 @@ class Base(Configuration):
"mailbox_oauth2",
*INSTALLED_PLUGINS,
# Third party apps
"django_zxcvbn_password_validator",
"drf_spectacular",
"drf_spectacular_sidecar", # required for Django collectstatic discovery
"corsheaders",
@@ -915,6 +916,47 @@ class Production(Base):
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# Password management
# - Password strength for ZxcvbnPasswordValidator
# 0 too guessable: risky password. (guesses < 10^3)
# 1 very guessable: protection from throttled online attacks. (guesses < 10^6)
# 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
# 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
# 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
PASSWORD_MINIMAL_STRENGTH = values.IntegerValue(
default=3,
environ_name="PASSWORD_MINIMAL_STRENGTH",
environ_prefix=None,
)
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
"OPTIONS": {
"user_attributes": (
"email", # for core.User
"name", # for core.User
"first_name", # for mailbox_manager.Mailbox
"last_name", # for mailbox_manager.Mailbox
"local_part", # for mailbox_manager.Mailbox
),
},
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
{
"NAME": "django_zxcvbn_password_validator.ZxcvbnPasswordValidator",
},
]
# For static files in production, we want to use a backend that includes a hash in
# the filename, that is calculated from the file content, so that browsers always
# get the updated version of each file.

View File

@@ -40,6 +40,7 @@ dependencies = [
"django-storages==1.14.5",
"django-timezone-field>=5.1",
"django-treebeard==4.7.1",
"django-zxcvbn-password-validator==1.4.5",
"django==5.1.7",
"djangorestframework==3.15.2",
"dockerflow==2024.4.2",