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',