From c236d60bd9fc4fc03bcc3552fb448c03e7c1c6b2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 14:27:57 +0200 Subject: [PATCH] Replaced django-passwords by django's builtins. see #1153 --- modoboa/admin/forms/account.py | 7 +- modoboa/admin/serializers.py | 6 +- modoboa/core/app_settings.py | 11 ++- .../core/commands/templates/settings.py.tpl | 32 ++++++--- modoboa/core/forms.py | 9 +-- modoboa/core/password_validation.py | 68 +++++++++++++++++++ requirements.txt | 1 - test_project/test_project/settings.py | 25 +++++++ 8 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 modoboa/core/password_validation.py diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 53e2ff5fe..6cbf0d581 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -10,7 +10,7 @@ from django.core.urlresolvers import reverse from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy -from passwords.fields import PasswordField +from django.contrib.auth import password_validation from modoboa.core import signals as core_signals from modoboa.core.models import User @@ -45,11 +45,11 @@ class AccountFormGeneral(forms.ModelForm): choices=[("", ugettext_lazy("Choose"))], help_text=ugettext_lazy("What level of permission this user will have") ) - password1 = PasswordField( + password1 = forms.CharField( label=ugettext_lazy("Password"), widget=forms.widgets.PasswordInput ) - password2 = PasswordField( + password2 = forms.CharField( label=ugettext_lazy("Confirmation"), widget=forms.widgets.PasswordInput, help_text=ugettext_lazy( @@ -157,6 +157,7 @@ class AccountFormGeneral(forms.ModelForm): if password1 != password2: raise forms.ValidationError( _("The two password fields didn't match.")) + password_validation.validate_password(password2, self.instance) return password2 def clean(self): diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 878df8b05..53ee877cd 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -7,8 +7,9 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.encoding import force_text +from django.contrib.auth import password_validation + from rest_framework import serializers -from passwords import validators from modoboa.admin import models as admin_models from modoboa.core import constants as core_constants @@ -159,8 +160,7 @@ class AccountPasswordSerializer(serializers.ModelSerializer): def validate_new_password(self, value): """Check new password.""" try: - validators.validate_length(value) - validators.complexity(value) + password_validation.validate_password(value, self.instance) except ValidationError as exc: raise serializers.ValidationError(exc.messages[0]) return value diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 47742a503..327ae3af3 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -6,7 +6,7 @@ from django import forms from django.conf import settings from django.utils.translation import ugettext as _, ugettext_lazy -from passwords import validators +from django.contrib.auth import password_validation from modoboa.lib import fields as lib_fields from modoboa.lib.cryptutils import random_key @@ -83,8 +83,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): label=ugettext_lazy("Default password"), initial="password", help_text=ugettext_lazy( - "Default password for automatically created accounts."), - validators=[validators.validate_length, validators.complexity] + "Default password for automatically created accounts.") ) # LDAP specific settings @@ -351,6 +350,12 @@ class GeneralParametersForm(param_forms.AdminParametersForm): raise forms.ValidationError(_("Invalid rounds number")) return value + def clean_default_password(self): + """Check password complexity.""" + value = self.cleaned_data["default_password"] + password_validation.validate_password(value) + return value + def clean(self): """Custom validation method diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index dcd50d733..2b9369149 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -178,15 +178,31 @@ REST_FRAMEWORK = { MODOBOA_API_URL = 'https://api.modoboa.org/1/' -# django-passwords +# Password validation rules -PASSWORD_MIN_LENGTH = 8 - -PASSWORD_COMPLEXITY = { - "UPPER": 1, - "LOWER": 1, - "DIGITS": 1 -} +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, + { + 'NAME': 'modoboa.core.password_validation.ComplexityValidator', + 'OPTIONS': { + 'upper': 1, + 'lower': 1, + 'digits': 1, + 'specials': 0 + } + }, +] # CKeditor diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py index c2a021f1f..b35c94fdb 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext as _, ugettext_lazy -from passwords.fields import PasswordField +from django.contrib.auth import password_validation from modoboa.core.models import User from modoboa.parameters import tools as param_tools @@ -37,11 +37,11 @@ class ProfileForm(forms.ModelForm): label=ugettext_lazy("Old password"), required=False, widget=forms.PasswordInput(attrs={"class": "form-control"}) ) - newpassword = PasswordField( + newpassword = forms.CharField( label=ugettext_lazy("New password"), required=False, widget=forms.PasswordInput(attrs={"class": "form-control"}) ) - confirmation = PasswordField( + confirmation = forms.CharField( label=ugettext_lazy("Confirmation"), required=False, widget=forms.PasswordInput(attrs={"class": "form-control"}) ) @@ -78,7 +78,8 @@ class ProfileForm(forms.ModelForm): confirmation = self.cleaned_data["confirmation"] if newpassword != confirmation: raise forms.ValidationError(_("Passwords mismatch")) - return self.cleaned_data["confirmation"] + password_validation.validate_password(confirmation, self.instance) + return confirmation def save(self, commit=True): user = super(ProfileForm, self).save(commit=False) diff --git a/modoboa/core/password_validation.py b/modoboa/core/password_validation.py new file mode 100644 index 000000000..e69dac511 --- /dev/null +++ b/modoboa/core/password_validation.py @@ -0,0 +1,68 @@ +"""Custom password validators.""" + +from __future__ import unicode_literals + +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext as _, ungettext + + +class ComplexityValidator(object): + """Check password contains at least a few things.""" + + def __init__(self, upper=1, lower=1, digits=1, specials=1): + self.upper = upper + self.lower = lower + self.digits = digits + self.specials = specials + + def validate(self, password, user=None): + special_characters = "~!@#$%^&*()_+{}\":;,'[]" + condition = ( + self.digits > 0 and + sum([1 for char in password if char.isdigit()]) < self.digits) + if condition: + raise ValidationError( + ungettext( + "Password must contain at least {} digit.", + "Password must contain at least {} digits.", + self.digits + ).format(self.digits)) + condition = ( + self.lower > 0 and + sum([1 for char in password if char.islower()]) < self.lower) + if condition: + raise ValidationError( + ungettext( + "Password must contain at least {} lowercase letter.", + "Password must contain at least {} lowercase letters.", + self.lower + ) + .format(self.lower)) + condition = ( + self.upper > 0 and + sum([1 for char in password if char.isupper()]) < self.upper) + if condition: + raise ValidationError( + ungettext( + "Password must contain at least {} uppercase letter.", + "Password must contain at least {} uppercase letters.", + self.upper + ) + .format(self.upper)) + condition = ( + self.specials > 0 and + sum([1 for char in password if char in special_characters]) < + self.specials) + if condition: + raise ValidationError( + ungettext( + "Password must contain at least {} special character.", + "Password must contain at least {} special characters.", + self.specials + ) + .format(self.specials)) + + def get_help_text(self): + return _( + "Your password must contain a combination of different " + "character types.") diff --git a/requirements.txt b/requirements.txt index 08dcfb547..e8abbe4ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ django>=1.10,<=1.10.99 django-ckeditor==5.2.1 -django-passwords==0.3.12 django-reversion==1.10.2 django-subcommand2==0.1.1 django-versionfield2>=0.5.0 diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 7493ab588..a5f762ba8 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -87,6 +87,31 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', ) +# Password validation rules +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, + { + 'NAME': 'modoboa.core.password_validation.ComplexityValidator', + 'OPTIONS': { + 'upper': 1, + 'lower': 1, + 'digits': 1, + 'specials': 0 + } + }, +] + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates',