Merge pull request 'Enforce stronger Passwords' (#38) from heinzel_dav_auth into master
All checks were successful
buildbot/tox Build done.
All checks were successful
buildbot/tox Build done.
Reviewed-on: #38
This commit was merged in pull request #38.
This commit is contained in:
@@ -62,7 +62,7 @@ class SetPasswordForm(forms.Form):
|
||||
return self.user
|
||||
|
||||
|
||||
class ResetPasswordForm(forms.Form):
|
||||
class CreateAndSendPasswordForm(forms.Form):
|
||||
username = auth_forms.UsernameField(
|
||||
max_length=254,
|
||||
label=_(u'E-Mail-Adresse'),
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="pull-right"><a href="{% url 'dav_auth:reset_password' %}">{% trans 'Passwort vergessen?' %}</a></div>
|
||||
<div class="pull-right"><a
|
||||
href="{% url 'dav_auth:recreate_password' %}">{% trans 'Passwort vergessen?' %}</a></div>
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-success">
|
||||
{% bootstrap_icon 'log-in' %} 
|
||||
|
||||
@@ -32,4 +32,38 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
|
||||
</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">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock page-container %}
|
||||
|
||||
@@ -9,7 +9,7 @@ from ..emails import PasswordSetEmail
|
||||
|
||||
|
||||
TEST_USERNAME = 'user'
|
||||
TEST_PASSWORD = u'me||ön 2'
|
||||
TEST_PASSWORD = u'me||ön 21ABll'
|
||||
TEST_EMAIL = 'root@localhost'
|
||||
|
||||
PASSWORD_EMAIL_TEMPLATE = u"""Hallo {fullname},
|
||||
|
||||
@@ -6,10 +6,10 @@ from django.utils.translation import ugettext
|
||||
|
||||
from dav_base.tests.generic import FormDataSet, FormsTestCase
|
||||
|
||||
from ..forms import LoginForm, SetPasswordForm, ResetPasswordForm
|
||||
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
|
||||
|
||||
TEST_USERNAME = 'root@localhost'
|
||||
TEST_PASSWORD = u'me||ön 2'
|
||||
TEST_PASSWORD = u'me||ön 21ABll'
|
||||
TEST_EMAIL = TEST_USERNAME
|
||||
USERNAME_MAX_LENGTH = 254
|
||||
|
||||
@@ -108,7 +108,7 @@ class SetPasswordFormTestCase(FormsTestCase):
|
||||
|
||||
def test_mismatch(self):
|
||||
data_sets = [
|
||||
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon13'},
|
||||
FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB13+-'},
|
||||
[('new_password_repeat', 'password_mismatch')]),
|
||||
]
|
||||
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):
|
||||
data_sets = [
|
||||
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12'}),
|
||||
FormDataSet({'new_password': 'mellon12', 'new_password_repeat': 'mellon12', 'send_password_mail': True}),
|
||||
FormDataSet({'new_password': u'"ä§ Mellon12', 'new_password_repeat': u'"ä§ Mellon12'}),
|
||||
FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-'}),
|
||||
FormDataSet({'new_password': 'mellonAB12+-', 'new_password_repeat': 'mellonAB12+-',
|
||||
'send_password_mail': True}),
|
||||
FormDataSet({'new_password': u'"ä§ MellonAB12+-', 'new_password_repeat': u'"ä§ MellonAB12+-'}),
|
||||
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})
|
||||
@@ -193,8 +194,8 @@ class SetPasswordFormTestCase(FormsTestCase):
|
||||
self.assertTrue(self.client.login(username=self.test_username, password=new_password))
|
||||
|
||||
|
||||
class ResetPasswordFormTestCase(FormsTestCase):
|
||||
form_class = ResetPasswordForm
|
||||
class CreateAndSendPasswordFormTestCase(FormsTestCase):
|
||||
form_class = CreateAndSendPasswordForm
|
||||
|
||||
valid_data_sets = (
|
||||
FormDataSet({'username': 'unittest@example.com'}),
|
||||
|
||||
@@ -9,7 +9,7 @@ from selenium.webdriver.common.keys import Keys
|
||||
from dav_base.tests.generic import ScreenshotTestCase
|
||||
|
||||
TEST_USERNAME = 'root@localhost'
|
||||
TEST_PASSWORD = u'me||ön 2'
|
||||
TEST_PASSWORD = u'me||ön 21ABll'
|
||||
TEST_EMAIL = TEST_USERNAME
|
||||
|
||||
|
||||
@@ -177,10 +177,10 @@ class TestCase(ScreenshotTestCase):
|
||||
self.wait_until_stale(c, password2_field)
|
||||
self.save_screenshot('set_password_succeed', sequence=sequence_name)
|
||||
|
||||
# Get password reset page -> since we are logged in, it should
|
||||
# Get password recreate page -> since we are logged in, it should
|
||||
# redirect to set password page again -> save
|
||||
html = c.find_element_by_tag_name('html')
|
||||
c.get(self.complete_url(reverse('dav_auth:reset_password')))
|
||||
c.get(self.complete_url(reverse('dav_auth:recreate_password')))
|
||||
self.wait_until_stale(c, html)
|
||||
self.wait_on_presence(c, (By.ID, 'id_new_password'))
|
||||
self.save_screenshot('empty_set_password_form', sequence=sequence_name)
|
||||
@@ -194,24 +194,24 @@ class TestCase(ScreenshotTestCase):
|
||||
self.wait_until_stale(c, user_menu)
|
||||
self.save_screenshot('logout_succeed', sequence=sequence_name)
|
||||
|
||||
# Click on 'login' to access password reset link
|
||||
# Click on 'login' to access password recreate link
|
||||
link = c.find_element_by_css_selector('#login-widget a')
|
||||
link.click()
|
||||
self.wait_on_presence(c, (By.ID, 'id_username'))
|
||||
|
||||
# Locate password reset link, click it -> save password reset form
|
||||
# Locate password recreate link, click it -> save password recreate form
|
||||
link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen'))
|
||||
link.click()
|
||||
username_field = self.wait_on_presence(c, (By.ID, 'id_username'))
|
||||
self.save_screenshot('empty_reset_password_form', sequence=sequence_name)
|
||||
self.save_screenshot('empty_recreate_password_form', sequence=sequence_name)
|
||||
|
||||
# Enter invalid username -> save result (login form, no message)
|
||||
username_field.send_keys(self.test_username[::-1])
|
||||
username_field.send_keys(Keys.RETURN)
|
||||
self.wait_until_stale(c, username_field)
|
||||
self.save_screenshot('reset_password_invalid_user', sequence=sequence_name)
|
||||
self.save_screenshot('recreate_password_invalid_user', sequence=sequence_name)
|
||||
|
||||
# Locate password reset link, click it
|
||||
# Locate password recreate link, click it
|
||||
link = c.find_element_by_partial_link_text(ugettext(u'Passwort vergessen'))
|
||||
link.click()
|
||||
username_field = self.wait_on_presence(c, (By.ID, 'id_username'))
|
||||
@@ -220,4 +220,4 @@ class TestCase(ScreenshotTestCase):
|
||||
username_field.send_keys(self.test_username)
|
||||
username_field.send_keys(Keys.RETURN)
|
||||
self.wait_until_stale(c, username_field)
|
||||
self.save_screenshot('reset_password_valid_user', sequence=sequence_name)
|
||||
self.save_screenshot('recreate_password_valid_user', sequence=sequence_name)
|
||||
|
||||
@@ -12,16 +12,16 @@ from dav_base.tests.generic import SeleniumTestCase
|
||||
from .generic import SeleniumAuthMixin
|
||||
|
||||
TEST_USERNAME = 'root@localhost'
|
||||
TEST_PASSWORD = 'me||ön 2'
|
||||
TEST_PASSWORD = 'me||ön 21ABll'
|
||||
TEST_EMAIL = TEST_USERNAME
|
||||
|
||||
|
||||
class TemplatesTestCase(SimpleTestCase):
|
||||
def test_reset_link_in_login_form(self):
|
||||
def test_recreate_link_in_login_form(self):
|
||||
login_url = reverse('dav_auth:login')
|
||||
reset_url = reverse('dav_auth:reset_password')
|
||||
recreate_url = reverse('dav_auth:recreate_password')
|
||||
text = ugettext('Passwort vergessen?')
|
||||
html = '<a href="{url}">{text}</a>'.format(url=reset_url, text=text)
|
||||
html = '<a href="{url}">{text}</a>'.format(url=recreate_url, text=text)
|
||||
response = self.client.get(login_url)
|
||||
self.assertInHTML(html, response.content.decode('utf-8'))
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@ class TestCase(UrlsTestCase):
|
||||
Url('/auth/logout', 'dav_auth:logout', views.LogoutView.as_view(), status_code=302),
|
||||
Url('/auth/password', 'dav_auth:set_password', views.SetPasswordView.as_view(),
|
||||
redirect='/auth/login?next=/auth/password'),
|
||||
Url('/auth/password/reset', 'dav_auth:reset_password', views.ResetPasswordView.as_view()),
|
||||
Url('/auth/password/recreate', 'dav_auth:recreate_password', views.CreateAndSendPasswordView.as_view()),
|
||||
)
|
||||
|
||||
227
dav_auth/tests/test_validators.py
Normal file
227
dav_auth/tests/test_validators.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from ..validators import PasswordScoreValidator, CustomWordlistPasswordValidator, CharacterClassPasswordValidator
|
||||
|
||||
|
||||
class PasswordScoreValidatorTestCase(SimpleTestCase):
|
||||
def test_too_little_score(self):
|
||||
passwords = [
|
||||
u'', # score = 0
|
||||
u'abcdefghijklmnopq', # score = 17
|
||||
u'Abcdefghijklmnop', # score = 16 + 1
|
||||
u'ABcdefghijklmno', # score = 15 + 2
|
||||
u'AB1defghijklmn', # score = 14 + 2 + 1
|
||||
u'AB12efghijklm', # score = 13 + 2 + 2
|
||||
u'AB12+fghijkl', # score = 12 + 2 + 2 + 1
|
||||
u'AB12+-ghijk', # score = 11 + 2 + 2 + 2
|
||||
u'AB12+-*hij', # score = 10 + 2 + 2 + 3
|
||||
u'AB12+-*hi/', # score = 10 + 2 + 2 + 3
|
||||
u'AB12+-*hi3', # score = 10 + 2 + 2 + 3
|
||||
u'AB12+-*hiC', # score = 10 + 2 + 2 + 3
|
||||
u'abcbbbgbbjklmnopqr', # score = 17
|
||||
]
|
||||
|
||||
validator = PasswordScoreValidator(min_classes=0)
|
||||
|
||||
for password in passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
self.assertEqual('too_little_score', e.code)
|
||||
else:
|
||||
self.fail(u'%s: no validation error was raised' % password)
|
||||
|
||||
def test_too_few_classes(self):
|
||||
passwords = [
|
||||
u'', # classes = 0
|
||||
u'abcdefgh', # classes = 1
|
||||
u'abcdef+-', # classes = 2
|
||||
u'abcd12gh', # classes = 2
|
||||
u'abcd12-+', # classes = 3
|
||||
u'abCDefgh', # classes = 2
|
||||
u'abCDef+-', # classes = 3
|
||||
u'abCD12gh', # classes = 3
|
||||
u'ABCD12-+', # classes = 3
|
||||
]
|
||||
|
||||
validator = PasswordScoreValidator(min_score=0, min_classes=4)
|
||||
|
||||
for password in passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
self.assertEqual('too_few_classes', e.code)
|
||||
else:
|
||||
self.fail(u'%s: no validation error was raised' % password)
|
||||
|
||||
def test_valid(self):
|
||||
passwords = [
|
||||
u'Abcdefghijklmnopq',
|
||||
u'ABcdefghijklmnop',
|
||||
u'AB1defghijklmno',
|
||||
u'AB12efghijklmn',
|
||||
u'AB12+fghijklm',
|
||||
u'AB12+-ghijkl',
|
||||
u'AB12+-*hijk',
|
||||
u'ab1defghijklmnopq',
|
||||
u'abcd+fghijklmnopq',
|
||||
u'AB1CDEFGHIJKLMNOPQ',
|
||||
]
|
||||
|
||||
validator = PasswordScoreValidator()
|
||||
|
||||
for password in passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
self.fail(e)
|
||||
|
||||
|
||||
class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
|
||||
def test_invalid(self):
|
||||
invalid_passwords = [
|
||||
(u'passwort', [
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'passwort\' enthalten.',
|
||||
]),
|
||||
(u'abcdDaVefgh', [
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'dav\' enthalten.',
|
||||
]),
|
||||
(u'abcdsektIonefgh', [
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'sektion\' enthalten.',
|
||||
]),
|
||||
(u'alpen12verein34KArlsruhE berge', [
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'berge\' enthalten.',
|
||||
]),
|
||||
(u'heinzel@alpenverein-karlsruhe.de', [
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'heinzel\' enthalten.',
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'alpenverein\' enthalten.',
|
||||
u'Das Passwort darf nicht die Zeichenfolge \'karlsruhe\' enthalten.',
|
||||
]),
|
||||
]
|
||||
|
||||
validator = CustomWordlistPasswordValidator()
|
||||
|
||||
for password, expected_errors in invalid_passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
errors = e.messages
|
||||
for expected_error in expected_errors:
|
||||
self.assertIn(expected_error, errors)
|
||||
for error in errors:
|
||||
self.assertIn(error, expected_errors)
|
||||
else:
|
||||
self.fail(u'%s: no validation error was raised' % password)
|
||||
|
||||
def test_valid(self):
|
||||
passwords = [
|
||||
u'',
|
||||
u'password',
|
||||
u'münchen',
|
||||
]
|
||||
|
||||
validator = CustomWordlistPasswordValidator()
|
||||
|
||||
for password in passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
self.fail(e)
|
||||
|
||||
|
||||
class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
|
||||
def setUp(self):
|
||||
super(CharacterClassPasswordValidatorTestCase, self).setUp()
|
||||
self.validator = CharacterClassPasswordValidator()
|
||||
|
||||
def test_invalid(self):
|
||||
invalid_passwords = [
|
||||
(u'', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'A+-', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
]),
|
||||
(u'1234567890*', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'34*/()', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
]),
|
||||
(u'AA', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'CD0.,', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
]),
|
||||
(u'EF56', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'8GH?!8', [
|
||||
u'Das Passwort muss mindestens 2 Kleinbuchstaben enthalten.',
|
||||
]),
|
||||
(u'bbX', [
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'$cd%', [
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
]),
|
||||
(u'ef90', [
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'1g=h3~', [
|
||||
u'Das Passwort muss mindestens 2 Großbuchstaben enthalten.',
|
||||
]),
|
||||
(u'Gi&jH', [
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
(u'IkK:i;', [
|
||||
u'Das Passwort muss mindestens 2 Ziffern enthalten.',
|
||||
]),
|
||||
(u'mKn4L8', [
|
||||
u'Das Passwort muss mindestens 2 Sonderzeichen enthalten.',
|
||||
]),
|
||||
]
|
||||
|
||||
validator = self.validator
|
||||
|
||||
for password, expected_errors in invalid_passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
errors = e.messages
|
||||
for expected_error in expected_errors:
|
||||
self.assertIn(expected_error, errors)
|
||||
for error in errors:
|
||||
self.assertIn(error, expected_errors)
|
||||
else:
|
||||
self.fail(u'%s: no validation error was raised' % password)
|
||||
|
||||
def test_valid(self):
|
||||
valid_passwords = [u'abCD12+-']
|
||||
validator = self.validator
|
||||
for password in valid_passwords:
|
||||
try:
|
||||
validator.validate(password)
|
||||
except ValidationError as e:
|
||||
self.fail(e)
|
||||
@@ -7,10 +7,10 @@ from django.test import TestCase
|
||||
from django.utils.translation import ugettext
|
||||
from django.urls import reverse
|
||||
|
||||
from ..forms import LoginForm, SetPasswordForm, ResetPasswordForm
|
||||
from ..forms import LoginForm, SetPasswordForm, CreateAndSendPasswordForm
|
||||
|
||||
TEST_USERNAME = 'root@localhost'
|
||||
TEST_PASSWORD = u'me||ön 2'
|
||||
TEST_PASSWORD = u'me||ön 21ABll'
|
||||
TEST_EMAIL = TEST_USERNAME
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class ViewsTestCase(TestCase):
|
||||
cls.logout_url = reverse('dav_auth:logout')
|
||||
cls.logout_redirect_url = resolve_url(cls.app_settings.logout_redirect_url)
|
||||
cls.set_password_url = reverse('dav_auth:set_password')
|
||||
cls.reset_password_url = reverse('dav_auth:reset_password')
|
||||
cls.recreate_password_url = reverse('dav_auth:recreate_password')
|
||||
|
||||
# Some messages
|
||||
cls.wrong_credentials_message = ugettext(u'Benutzername oder Passwort falsch.')
|
||||
@@ -179,23 +179,23 @@ class ViewsTestCase(TestCase):
|
||||
self.assertFalse(self.client.login(username=username, password=password), 'Old password still valid')
|
||||
self.assertTrue(self.client.login(username=username, password=new_password), 'New password not valid')
|
||||
|
||||
def test_reset_password_integrated_unauth_get(self):
|
||||
response = self.client.get(self.reset_password_url)
|
||||
def test_recreate_password_integrated_unauth_get(self):
|
||||
response = self.client.get(self.recreate_password_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'dav_auth/forms/reset_password.html')
|
||||
self.assertTemplateUsed(response, 'dav_auth/forms/recreate_password.html')
|
||||
self.assertIn('form', response.context)
|
||||
self.assertIsInstance(response.context['form'], ResetPasswordForm)
|
||||
self.assertIsInstance(response.context['form'], CreateAndSendPasswordForm)
|
||||
|
||||
field = response.context['form'].fields['username']
|
||||
self.assertTrue(field.required)
|
||||
|
||||
def test_reset_password_integrated_auth_get(self):
|
||||
def test_recreate_password_integrated_auth_get(self):
|
||||
self.client.login(username=self.test_username, password=self.test_password)
|
||||
response = self.client.get(self.reset_password_url)
|
||||
response = self.client.get(self.recreate_password_url)
|
||||
self.assertRedirects(response, self.set_password_url)
|
||||
|
||||
def test_reset_password_integrated_post(self):
|
||||
location = self.reset_password_url
|
||||
def test_recreate_password_integrated_post(self):
|
||||
location = self.recreate_password_url
|
||||
|
||||
response = self.client.post(location, {'username': self.user.username})
|
||||
self.assertRedirects(response, self.login_url)
|
||||
|
||||
@@ -6,5 +6,5 @@ urlpatterns = [
|
||||
url(r'^login$', views.LoginView.as_view(), name='login'),
|
||||
url(r'^logout$', views.LogoutView.as_view(), name='logout'),
|
||||
url(r'^password$', views.SetPasswordView.as_view(), name='set_password'),
|
||||
url(r'^password/reset$', views.ResetPasswordView.as_view(), name='reset_password'),
|
||||
url(r'^password/recreate$', views.CreateAndSendPasswordView.as_view(), name='recreate_password'),
|
||||
]
|
||||
|
||||
200
dav_auth/validators.py
Normal file
200
dav_auth/validators.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class PasswordScoreValidator(object):
|
||||
def _get_score(self, password, user=None):
|
||||
score = 0
|
||||
char_counters = {}
|
||||
used_classes = []
|
||||
credits = {
|
||||
'lower': self.lcredit,
|
||||
'upper': self.ucredit,
|
||||
'digit': self.dcredit,
|
||||
'other': self.ocredit,
|
||||
}
|
||||
|
||||
for c in password:
|
||||
if c not in char_counters:
|
||||
char_counters[c] = 1
|
||||
else:
|
||||
char_counters[c] += 1
|
||||
if (self.max_repeat > 0) and (char_counters[c] > self.max_repeat):
|
||||
continue
|
||||
|
||||
score += 1
|
||||
|
||||
if c.isalpha() and c.islower():
|
||||
char_class = 'lower'
|
||||
elif c.isupper():
|
||||
char_class = 'upper'
|
||||
elif c.isdigit():
|
||||
char_class = 'digit'
|
||||
else:
|
||||
char_class = 'other'
|
||||
|
||||
if char_class not in used_classes:
|
||||
used_classes.append(char_class)
|
||||
if credits[char_class] > 0:
|
||||
score += 1
|
||||
credits[char_class] -= 1
|
||||
|
||||
return score, len(used_classes)
|
||||
|
||||
def __init__(self, min_score=18, max_repeat=5, lcredit=0, ucredit=2, dcredit=2, ocredit=3, min_classes=2):
|
||||
self.min_score = min_score
|
||||
self.max_repeat = max_repeat
|
||||
self.lcredit = lcredit
|
||||
self.ucredit = ucredit
|
||||
self.dcredit = dcredit
|
||||
self.ocredit = ocredit
|
||||
self.min_classes = min_classes
|
||||
|
||||
def validate(self, password, user=None):
|
||||
score, used_classes = self._get_score(password, user=user)
|
||||
|
||||
if used_classes < self.min_classes:
|
||||
raise ValidationError(_(u'Das Passwort muss Zeichen aus mindestens %(min_classes)d'
|
||||
u' verschiedenen Arten von Zeichen bestehen'
|
||||
u' (d.h. Kleinbuchstaben, Großbuchstaben, Ziffern und Sonderzeichen).'),
|
||||
code='too_few_classes',
|
||||
params={'min_classes': self.min_classes})
|
||||
|
||||
if score < self.min_score:
|
||||
raise ValidationError(_(u'Dieses Passwort ist zu einfach. Benutze mehr Zeichen'
|
||||
u' und gegebenenfalls auch Großbuchstaben, Ziffern und Sonderzeichen.'),
|
||||
code='too_little_score')
|
||||
|
||||
return score
|
||||
|
||||
def get_help_text(self):
|
||||
text = u'%s\n%s' % (
|
||||
_(u'The password must get a minimum score of %d points.') % self.min_score,
|
||||
_(u'For each character the score is increased by 1 point.'),
|
||||
)
|
||||
if self.lcredit > 0:
|
||||
text += '\n'
|
||||
text += _(u'The first %d lower characters increase the score by 1 point each.') % self.lcredit
|
||||
if self.ucredit > 0:
|
||||
text += '\n'
|
||||
text += _(u'The first %d upper characters increase the score by 1 point each.') % self.ucredit
|
||||
if self.dcredit > 0:
|
||||
text += '\n'
|
||||
text += _(u'The first %d digits increase the score by 1 point each.') % self.dcredit
|
||||
if self.ocredit > 0:
|
||||
text += '\n'
|
||||
text += _(u'The first %d non alpha numeric characters'
|
||||
u' increase the score by 1 point each.') % self.ocredit
|
||||
if self.max_repeat > 0:
|
||||
text += '\n'
|
||||
text += _(u'If a particular character is used more than %d times,'
|
||||
u' it will not increase the score anymore.') % self.max_repeat
|
||||
if self.min_classes > 0:
|
||||
text += '\n'
|
||||
text += _(u'Also the password must contain characters from %d different character classes'
|
||||
u' (i.e. lower, upper, digits, others).') % self.min_classes
|
||||
return text
|
||||
|
||||
|
||||
class CustomWordlistPasswordValidator(object):
|
||||
context = 'the Sektion Karlsruhe'
|
||||
words = (
|
||||
u'dav',
|
||||
u'berge',
|
||||
u'sektion',
|
||||
u'karlsruhe',
|
||||
u'alpenverein',
|
||||
u'heinzel',
|
||||
u'passwort',
|
||||
)
|
||||
|
||||
def validate(self, password, user=None):
|
||||
errors = []
|
||||
|
||||
lower_pw = password.lower()
|
||||
for word in self.words:
|
||||
if word in lower_pw:
|
||||
error = ValidationError(_(u'Das Passwort darf nicht die Zeichenfolge \'%(word)s\' enthalten.'),
|
||||
code='forbidden_word',
|
||||
params={'word': word})
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def get_help_text(self):
|
||||
text = _(u'The password must not contain some specific words,'
|
||||
u' that are common in context with %(context)s.') % {'context': self.context}
|
||||
text += u'\n'
|
||||
text += _(u'All words are matched case insensitive.')
|
||||
return text
|
||||
|
||||
|
||||
class CharacterClassPasswordValidator(object):
|
||||
def _is_enough_lower(self, password):
|
||||
lower = re.sub(r'[^a-z]', '', password)
|
||||
if len(lower) < self.minimum_lower:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _is_enough_upper(self, password):
|
||||
upper = re.sub(r'[^A-Z]', '', password)
|
||||
if len(upper) < self.minimum_upper:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _is_enough_digits(self, password):
|
||||
digits = re.sub(r'[^0-9]', '', password)
|
||||
if len(digits) < self.minimum_digits:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _is_enough_others(self, password):
|
||||
others = re.sub(r'[a-zA-Z0-9]', '', password)
|
||||
if len(others) < self.minimum_others:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __init__(self, minimum_lower=2, minimum_upper=2, minimum_digits=2, minimum_others=2):
|
||||
self.minimum_lower = minimum_lower
|
||||
self.minimum_upper = minimum_upper
|
||||
self.minimum_digits = minimum_digits
|
||||
self.minimum_others = minimum_others
|
||||
|
||||
def validate(self, password, user=None):
|
||||
errors = []
|
||||
if not self._is_enough_lower(password):
|
||||
error = ValidationError(_(u'Das Passwort muss mindestens %(min_lower)d Kleinbuchstaben enthalten.'),
|
||||
code='too_few_lower_characters',
|
||||
params={'min_lower': self.minimum_lower})
|
||||
errors.append(error)
|
||||
if not self._is_enough_upper(password):
|
||||
error = ValidationError(_(u'Das Passwort muss mindestens %(min_upper)d Großbuchstaben enthalten.'),
|
||||
code='too_few_upper_characters',
|
||||
params={'min_upper': self.minimum_upper})
|
||||
errors.append(error)
|
||||
if not self._is_enough_digits(password):
|
||||
error = ValidationError(_(u'Das Passwort muss mindestens %(min_digits)d Ziffern enthalten.'),
|
||||
code='too_few_digits',
|
||||
params={'min_digits': self.minimum_digits})
|
||||
errors.append(error)
|
||||
if not self._is_enough_others(password):
|
||||
error = ValidationError(_(u'Das Passwort muss mindestens %(min_others)d'
|
||||
u' Sonderzeichen enthalten.'),
|
||||
code='too_few_other_characters',
|
||||
params={'min_others': self.minimum_others})
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def get_help_text(self):
|
||||
text = u'%s %s %s %s' % (
|
||||
_('The password must contain at least %d characters from a-z.') % self.minimum_lower,
|
||||
_('The password must contain at least %d characters from A-Z.') % self.minimum_upper,
|
||||
_('The password must contain at least %d digits from 0-9.') % self.minimum_digits,
|
||||
_('The password must contain at least %d non alpha numeric characters.') % self.minimum_others,
|
||||
)
|
||||
return text
|
||||
@@ -1,10 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from django.apps import apps
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import views as auth_views, get_user_model
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import resolve_url
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import generic
|
||||
|
||||
@@ -28,6 +32,18 @@ class LoginView(auth_views.LoginView):
|
||||
def form_valid(self, form):
|
||||
r = super(LoginView, self).form_valid(form)
|
||||
messages.success(self.request, _(u'Benutzer angemeldet: %(username)s') % {'username': form.get_user()})
|
||||
try:
|
||||
validate_password(form.cleaned_data['password'])
|
||||
except ValidationError as e:
|
||||
logger.warning(u'Weak password (%d): %s', self.request.user.pk, e)
|
||||
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
|
||||
|
||||
|
||||
@@ -60,9 +76,9 @@ class SetPasswordView(auth_views.PasswordChangeView):
|
||||
return r
|
||||
|
||||
|
||||
class ResetPasswordView(generic.FormView):
|
||||
form_class = forms.ResetPasswordForm
|
||||
template_name = 'dav_auth/forms/reset_password.html'
|
||||
class CreateAndSendPasswordView(generic.FormView):
|
||||
form_class = forms.CreateAndSendPasswordForm
|
||||
template_name = 'dav_auth/forms/recreate_password.html'
|
||||
success_url = reverse_lazy('dav_auth:login')
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -70,19 +86,19 @@ class ResetPasswordView(generic.FormView):
|
||||
user_model = get_user_model()
|
||||
try:
|
||||
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.save()
|
||||
email = emails.PasswordSetEmail(user, random_password)
|
||||
email.send()
|
||||
messages.success(self.request, _(u'Neues Passwort versendet.'))
|
||||
logger.info('Password reset for user \'%s\'', username)
|
||||
logger.info('Password recreated for user \'%s\'', username)
|
||||
except user_model.DoesNotExist:
|
||||
logger.warning('Password reset for unknown user \'%s\'', username)
|
||||
logger.warning('Password recreated for unknown user \'%s\'', username)
|
||||
|
||||
return super(ResetPasswordView, self).form_valid(form)
|
||||
return super(CreateAndSendPasswordView, self).form_valid(form)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse('dav_auth:set_password'))
|
||||
return super(ResetPasswordView, self).get(request, *args, **kwargs)
|
||||
return super(CreateAndSendPasswordView, self).get(request, *args, **kwargs)
|
||||
|
||||
@@ -52,6 +52,30 @@ DATABASES['default'] = {
|
||||
'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')
|
||||
|
||||
LANGUAGE_CODE = 'de'
|
||||
|
||||
@@ -14,7 +14,7 @@ from dav_auth.tests.generic import SeleniumAuthMixin
|
||||
from .generic import RoleMixin
|
||||
|
||||
TEST_TRAINER_EMAIL = 'trainer@localhost'
|
||||
TEST_PASSWORD = u'me||ön 2'
|
||||
TEST_PASSWORD = u'me||ön 21ABll'
|
||||
TEST_EVENT_DATA_S = {
|
||||
'mode': 'training',
|
||||
'sport': 'S',
|
||||
|
||||
Reference in New Issue
Block a user