diff --git a/README.rst b/README.rst index c8ed5c080..2e6247a5e 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,12 @@ Documentation A detailed `documentation `_ will help you to install, use or extend Modoboa. +***************** +Demo Installation +***************** + +If you want to try out Modoboa, check out our `demo installation `_. + ************* External code ************* diff --git a/doc/manual_installation/modoboa.rst b/doc/manual_installation/modoboa.rst index e61d05d34..ff2205892 100644 --- a/doc/manual_installation/modoboa.rst +++ b/doc/manual_installation/modoboa.rst @@ -23,7 +23,7 @@ The following example illustrates how to realize this (Debian like system): > sudo useradd modoboa > sudo -i modoboa > virtualenv env - (env)> source env/bin/activate + > source env/bin/activate (env)> pip install -U pip Modoboa depends on external tools and some of them require compilation diff --git a/ldap-requirements.txt b/ldap-requirements.txt index 212b582a2..35eaac975 100644 --- a/ldap-requirements.txt +++ b/ldap-requirements.txt @@ -1 +1 @@ -django-auth-ldap +django-auth-ldap>=1.2.13 diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 53e2ff5fe..7dedf3285 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,8 @@ class AccountFormGeneral(forms.ModelForm): if password1 != password2: raise forms.ValidationError( _("The two password fields didn't match.")) + if password2 != "": + password_validation.validate_password(password2, self.instance) return password2 def clean(self): @@ -181,9 +183,8 @@ class AccountFormGeneral(forms.ModelForm): raise lib_exceptions.PermDeniedException( _("You can't disable your own account")) if commit: - if "password1" in self.cleaned_data \ - and self.cleaned_data["password1"] != "": - account.set_password(self.cleaned_data["password1"]) + if self.cleaned_data.get("password2", "") != "": + account.set_password(self.cleaned_data["password2"]) account.save() account.role = self.cleaned_data["role"] return account diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 878df8b05..6a12a4b06 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 @@ -198,8 +198,7 @@ class WritableAccountSerializer(AccountSerializer): def validate_password(self, value): """Check password constraints.""" 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 733d2374e..57875b749 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy from django.contrib.auth import forms as auth_forms from django.contrib.auth import get_user_model -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 @@ -41,11 +41,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"}) ) @@ -82,7 +82,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/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index df9894f1e..995c24272 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -108,7 +108,7 @@ try: def authenticate(self, username, password): if self.global_params["authentication_type"] == "ldap": return super(LDAPBackend, self).authenticate( - username, password) + username=username, password=password) return None except ImportError: 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',