From 6fb8221f3092a07aa90c4d822a5234d57752d429 Mon Sep 17 00:00:00 2001 From: Frank Lanitz Date: Sat, 17 Jun 2017 12:33:53 +0200 Subject: [PATCH 1/6] Remove maybe missleading prefix At this very point you are not yet inside a virtualenv. --- doc/manual_installation/modoboa.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 11ea0867b087c612b4dca06e0b9557eeabed17de Mon Sep 17 00:00:00 2001 From: Frank Lanitz Date: Mon, 19 Jun 2017 10:26:47 +0200 Subject: [PATCH 2/6] Add hint to demo installation to global README This should fix #1141 /at least it does for me). --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) 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 ************* From 83299b9e8f3e8922cba5ed7ede01b0dcf27f0dcc Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 15:31:17 +0200 Subject: [PATCH 3/6] Fixed custom authentication backend. see #1155 --- ldap-requirements.txt | 2 +- modoboa/lib/authbackends.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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: From c236d60bd9fc4fc03bcc3552fb448c03e7c1c6b2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 14:27:57 +0200 Subject: [PATCH 4/6] 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', From 109329ae056a9123a5780dbdffdc44829f480c2e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 14:55:22 +0200 Subject: [PATCH 5/6] Fixed validation method. see #1153 --- modoboa/admin/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modoboa/admin/serializers.py b/modoboa/admin/serializers.py index 53ee877cd..6a12a4b06 100644 --- a/modoboa/admin/serializers.py +++ b/modoboa/admin/serializers.py @@ -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 From ba2014e4f0ad80cb96a726c80f04c0baf5104f65 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Jun 2017 15:16:35 +0200 Subject: [PATCH 6/6] Don't check password if empty. see #1153 --- modoboa/admin/forms/account.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 6cbf0d581..7dedf3285 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -157,7 +157,8 @@ class AccountFormGeneral(forms.ModelForm): if password1 != password2: raise forms.ValidationError( _("The two password fields didn't match.")) - password_validation.validate_password(password2, self.instance) + if password2 != "": + password_validation.validate_password(password2, self.instance) return password2 def clean(self): @@ -182,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