UPD: enable stronger password validation and eventually warning message on login
All checks were successful
buildbot/tox Build done.

This commit is contained in:
2020-12-22 18:42:06 +01:00
parent 47dd196c6a
commit c3f72a50ff
11 changed files with 132 additions and 63 deletions

View File

@@ -32,4 +32,38 @@
   
</div> </div>
</div> </div>
<div class="row">
<div class="col-sm-2">
&nbsp;
</div>
<div class="col-sm-8">
<h3 class="top-most">{% trans 'Passwortrichtlinien' %}</h3>
<div class="well">
<p>
Damit die persönlichen Daten unserer Teilnehmer jederzeit geschützt sind, ist es unabdingbar,
dass eure Passwörter für das Touren- & Kurse-Portal nicht einfach zu erraten sind.
</p>
<p>
Je länger das Passwort ist und je mehr <i>ungewöhnliche</i> Zeichen darin enthalten sind,
umso unwahrscheinlicher ist es, dass das Passwort erraten werden kann.
</p>
<p>
Um sicher zu stellen, dass eure Passwörter ausreichend lang sind und nicht nur
aus einfachen Wörtern bestehen, wird die Güte eures Passwortes mit Punkten bewertet.<br />
Ausserdem dürfen bestimmte Wörter nicht enthalten sein (z.B. Karlsruhe).
</p>
<p>
Mehr Zeichen geben mehr Punkte. Und Großbuchstaben, Ziffern und Sonderzeichen geben Extrapunkte.
</p>
<p>
Im Regelfall sollte euer Passwort aus mindestens 12 Zeichen bestehen,
darunter mindestens je zwei Großbuchstaben, Ziffern und Sonderzeichen.<br />
Nehmt da ruhig alles, was eure Tastatur so hergibt.
</p>
</div>
</div>
<div class="col-sm-3">
&nbsp;
</div>
</div>
{% endblock page-container %} {% endblock page-container %}

View File

@@ -9,7 +9,7 @@ from ..emails import PasswordSetEmail
TEST_USERNAME = 'user' TEST_USERNAME = 'user'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = 'root@localhost' TEST_EMAIL = 'root@localhost'
PASSWORD_EMAIL_TEMPLATE = u"""Hallo {fullname}, PASSWORD_EMAIL_TEMPLATE = u"""Hallo {fullname},

View File

@@ -9,7 +9,7 @@ from dav_base.tests.generic import FormDataSet, FormsTestCase
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME
USERNAME_MAX_LENGTH = 254 USERNAME_MAX_LENGTH = 254
@@ -108,7 +108,7 @@ class SetPasswordFormTestCase(FormsTestCase):
def test_mismatch(self): def test_mismatch(self):
data_sets = [ data_sets = [
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon13'}, FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB13+-'},
[('new_password_repeat', 'password_mismatch')]), [('new_password_repeat', 'password_mismatch')]),
] ]
super(SetPasswordFormTestCase, self).test_invalid_data(data_sets=data_sets, form_kwargs={'user': self.user}) super(SetPasswordFormTestCase, self).test_invalid_data(data_sets=data_sets, form_kwargs={'user': self.user})
@@ -150,9 +150,10 @@ class SetPasswordFormTestCase(FormsTestCase):
def test_valid(self): def test_valid(self):
data_sets = [ data_sets = [
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12'}), FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-'}),
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12', 'send_password_mail': True}), FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-',
FormDataSet({'new_password': u'"ä§ Mellon12', 'new_password_repeat': u'"ä§ Mellon12'}), 'send_password_mail': True}),
FormDataSet({'new_password': u'"ä§ MellonAB12+-', 'new_password_repeat': u'"ä§ MellonAB12+-'}),
FormDataSet({'new_password': 'mellon12' * 128, 'new_password_repeat': 'mellon12' * 128}), FormDataSet({'new_password': 'mellon12' * 128, 'new_password_repeat': 'mellon12' * 128}),
] ]
super(SetPasswordFormTestCase, self).test_valid_data(data_sets=data_sets, form_kwargs={'user': self.user}) super(SetPasswordFormTestCase, self).test_valid_data(data_sets=data_sets, form_kwargs={'user': self.user})

View File

@@ -9,7 +9,7 @@ from selenium.webdriver.common.keys import Keys
from dav_base.tests.generic import ScreenshotTestCase from dav_base.tests.generic import ScreenshotTestCase
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME

View File

@@ -12,7 +12,7 @@ from dav_base.tests.generic import SeleniumTestCase
from .generic import SeleniumAuthMixin from .generic import SeleniumAuthMixin
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = 'me||ön 2' TEST_PASSWORD = 'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME

View File

@@ -83,22 +83,22 @@ class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
def test_invalid(self): def test_invalid(self):
invalid_passwords = [ invalid_passwords = [
(u'passwort', [ (u'passwort', [
u'The password must not contain the word \'passwort\'', u'Das Passwort darf nicht die Zeichenfolge \'passwort\' enthalten.',
]), ]),
(u'abcdDaVefgh', [ (u'abcdDaVefgh', [
u'The password must not contain the word \'dav\'', u'Das Passwort darf nicht die Zeichenfolge \'dav\' enthalten.',
]), ]),
(u'abcdsektIonefgh', [ (u'abcdsektIonefgh', [
u'The password must not contain the word \'sektion\'', u'Das Passwort darf nicht die Zeichenfolge \'sektion\' enthalten.',
]), ]),
(u'alpen12verein34KArlsruhE berge', [ (u'alpen12verein34KArlsruhE berge', [
u'The password must not contain the word \'karlsruhe\'', u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
u'The password must not contain the word \'berge\'', u'Das Passwort darf nicht die Zeichenfolge \'berge\' enthalten.',
]), ]),
(u'heinzel@alpenverein-karlsruhe.de', [ (u'heinzel@alpenverein-karlsruhe.de', [
u'The password must not contain the word \'heinzel\'', u'Das Passwort darf nicht die Zeichenfolge \'heinzel\' enthalten.',
u'The password must not contain the word \'alpenverein\'', u'Das Passwort darf nicht die Zeichenfolge \'alpenverein\' enthalten.',
u'The password must not contain the word \'karlsruhe\'', u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
]), ]),
] ]
@@ -140,66 +140,66 @@ class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
def test_invalid(self): def test_invalid(self):
invalid_passwords = [ invalid_passwords = [
(u'', [ (u'', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'A+-', [ (u'A+-', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]), ]),
(u'1234567890*', [ (u'1234567890*', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'34*/()', [ (u'34*/()', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
]), ]),
(u'AA', [ (u'AA', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'CD0.,', [ (u'CD0.,', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]), ]),
(u'EF56', [ (u'EF56', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'8GH?!8', [ (u'8GH?!8', [
u'The password must contain at least 2 characters from a-z', u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
]), ]),
(u'bbX', [ (u'bbX', [
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'$cd%', [ (u'$cd%', [
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]), ]),
(u'ef90', [ (u'ef90', [
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'1g=h3~', [ (u'1g=h3~', [
u'The password must contain at least 2 characters from A-Z', u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
]), ]),
(u'Gi&jH', [ (u'Gi&jH', [
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
(u'IkK:i;', [ (u'IkK:i;', [
u'The password must contain at least 2 digits from 0-9', u'Das Passwort muss mindestens 2 Ziffern enthalten.',
]), ]),
(u'mKn4L8', [ (u'mKn4L8', [
u'The password must contain at least 2 non alpha numeric characters', u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
]), ]),
] ]

View File

@@ -10,7 +10,7 @@ from django.urls import reverse
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
TEST_USERNAME = 'root@localhost' TEST_USERNAME = 'root@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EMAIL = TEST_USERNAME TEST_EMAIL = TEST_USERNAME

View File

@@ -56,15 +56,15 @@ class PasswordScoreValidator(object):
score, used_classes = self._get_score(password, user=user) score, used_classes = self._get_score(password, user=user)
if used_classes < self.min_classes: if used_classes < self.min_classes:
raise ValidationError(_(u'The password must contain characters from at least %(min_classes)d' raise ValidationError(_(u'Das Passwort muss Zeichen aus mindestens %(min_classes)d'
u' different character classes (i.e. lower, upper, digits, others).'), u' verschiedenen Arten von Zeichen bestehen'
u' (d.h. Kleinbuchstaben, Großbuchstaben, Ziffern und Sonderzeichen).'),
code='too_few_classes', code='too_few_classes',
params={'min_classes': self.min_classes}) params={'min_classes': self.min_classes})
if score < self.min_score: if score < self.min_score:
raise ValidationError(_(u'The password is too simple. Use more characters' raise ValidationError(_(u'Dieses Passwort ist zu einfach. Benutze mehr Zeichen'
u' and maybe use more different character classes' u' und gegebenenfalls auch Großbuchstaben, Ziffern und Sonderzeichen.'),
u' (i.e. lower, upper, digits, others).'),
code='too_little_score') code='too_little_score')
return score return score
@@ -116,7 +116,7 @@ class CustomWordlistPasswordValidator(object):
lower_pw = password.lower() lower_pw = password.lower()
for word in self.words: for word in self.words:
if word in lower_pw: if word in lower_pw:
error = ValidationError(_(u'The password must not contain the word \'%(word)s\''), error = ValidationError(_(u'Das Passwort darf nicht die Zeichenfolge \'%(word)s\' enthalten.'),
code='forbidden_word', code='forbidden_word',
params={'word': word}) params={'word': word})
errors.append(error) errors.append(error)
@@ -166,23 +166,23 @@ class CharacterClassPasswordValidator(object):
def validate(self, password, user=None): def validate(self, password, user=None):
errors = [] errors = []
if not self._is_enough_lower(password): if not self._is_enough_lower(password):
error = ValidationError(_(u'The password must contain at least %(min_lower)d characters from a-z'), error = ValidationError(_(u'Das Passwort muss mindestens %(min_lower)d Kleinbuchstaben enthalten.'),
code='too_few_lower_characters', code='too_few_lower_characters',
params={'min_lower': self.minimum_lower}) params={'min_lower': self.minimum_lower})
errors.append(error) errors.append(error)
if not self._is_enough_upper(password): if not self._is_enough_upper(password):
error = ValidationError(_(u'The password must contain at least %(min_upper)d characters from A-Z'), error = ValidationError(_(u'Das Passwort muss mindestens %(min_upper)d Großbuchstaben enthalten.'),
code='too_few_upper_characters', code='too_few_upper_characters',
params={'min_upper': self.minimum_upper}) params={'min_upper': self.minimum_upper})
errors.append(error) errors.append(error)
if not self._is_enough_digits(password): if not self._is_enough_digits(password):
error = ValidationError(_(u'The password must contain at least %(min_digits)d digits from 0-9'), error = ValidationError(_(u'Das Passwort muss mindestens %(min_digits)d Ziffern enthalten.'),
code='too_few_digits', code='too_few_digits',
params={'min_digits': self.minimum_digits}) params={'min_digits': self.minimum_digits})
errors.append(error) errors.append(error)
if not self._is_enough_others(password): if not self._is_enough_others(password):
error = ValidationError(_(u'The password must contain at least %(min_others)d' error = ValidationError(_(u'Das Passwort muss mindestens %(min_others)d'
u' non alpha numeric characters'), u' Sonderzeichen enthalten.'),
code='too_few_other_characters', code='too_few_other_characters',
params={'min_others': self.minimum_others}) params={'min_others': self.minimum_others})
errors.append(error) errors.append(error)

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import logging import logging
from django.apps import apps from django.apps import apps
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@@ -7,6 +8,7 @@ from django.contrib.auth.password_validation import validate_password
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import generic from django.views import generic
@@ -29,11 +31,19 @@ class LoginView(auth_views.LoginView):
def form_valid(self, form): def form_valid(self, form):
r = super(LoginView, self).form_valid(form) r = super(LoginView, self).form_valid(form)
messages.success(self.request, _(u'Benutzer angemeldet: %(username)s') % {'username': form.get_user()})
try: try:
validate_password(form.cleaned_data['password']) validate_password(form.cleaned_data['password'])
except ValidationError as e: except ValidationError as e:
logger.warning(u'Weak password (%d): %s', self.request.user.pk, e) logger.warning(u'Weak password (%d): %s', self.request.user.pk, e)
messages.success(self.request, _(u'Benutzer angemeldet: %(username)s') % {'username': form.get_user()}) message = u'<br />\n<p>\n'
message += u'Dein Passwort entspricht nicht mehr den aktuellen Passwortrichtlinien.<br />\n'
message += u'Bitte hilf uns die Daten deiner Teilnehmer zu schützen und ändere dein Passwort.<br />\n'
message += u'</p>\n'
message += u'<p>\n'
message += u'<a href="%(href)s">Passwort ändern</a>\n' % {'href': reverse('dav_auth:set_password')}
message += u'</p>\n<br />\n'
messages.warning(self.request, mark_safe(message))
return r return r
@@ -76,7 +86,7 @@ class CreateAndSendPasswordView(generic.FormView):
user_model = get_user_model() user_model = get_user_model()
try: try:
user = user_model.objects.get(username=username) user = user_model.objects.get(username=username)
random_password = user_model.objects.make_random_password(length=12) random_password = user_model.objects.make_random_password(length=32)
user.set_password(random_password) user.set_password(random_password)
user.save() user.save()
email = emails.PasswordSetEmail(user, random_password) email = emails.PasswordSetEmail(user, random_password)

View File

@@ -52,6 +52,30 @@ DATABASES['default'] = {
'NAME': os.path.join(BASE_VAR_DIR, 'db', 'devel.sqlite3'), 'NAME': os.path.join(BASE_VAR_DIR, 'db', 'devel.sqlite3'),
} }
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
},
},
{
'NAME': 'dav_auth.validators.PasswordScoreValidator',
},
{
'NAME': 'dav_auth.validators.CustomWordlistPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
STATIC_ROOT = os.path.join(BASE_VAR_DIR, 'www', 'static') STATIC_ROOT = os.path.join(BASE_VAR_DIR, 'www', 'static')
LANGUAGE_CODE = 'de' LANGUAGE_CODE = 'de'

View File

@@ -14,7 +14,7 @@ from dav_auth.tests.generic import SeleniumAuthMixin
from .generic import RoleMixin from .generic import RoleMixin
TEST_TRAINER_EMAIL = 'trainer@localhost' TEST_TRAINER_EMAIL = 'trainer@localhost'
TEST_PASSWORD = u'me||ön 2' TEST_PASSWORD = u'me||ön 21ABll'
TEST_EVENT_DATA_S = { TEST_EVENT_DATA_S = {
'mode': 'training', 'mode': 'training',
'sport': 'S', 'sport': 'S',