UPD: auth: added a custom wordlist password validator

This commit is contained in:
2020-12-22 17:04:40 +01:00
parent 0762876ec5
commit 47dd196c6a
2 changed files with 213 additions and 117 deletions

View File

@@ -2,25 +2,25 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import SimpleTestCase from django.test import SimpleTestCase
from ..validators import PasswordScoreValidator, CharacterClassPasswordValidator from ..validators import PasswordScoreValidator, CustomWordlistPasswordValidator, CharacterClassPasswordValidator
class PasswordScoreValidatorTestCase(SimpleTestCase): class PasswordScoreValidatorTestCase(SimpleTestCase):
def test_too_little_score(self): def test_too_little_score(self):
passwords = [ passwords = [
'', # score = 0 u'', # score = 0
'abcdefghijklmnopq', # score = 17 u'abcdefghijklmnopq', # score = 17
'Abcdefghijklmnop', # score = 16 + 1 u'Abcdefghijklmnop', # score = 16 + 1
'ABcdefghijklmno', # score = 15 + 2 u'ABcdefghijklmno', # score = 15 + 2
'AB1defghijklmn', # score = 14 + 2 + 1 u'AB1defghijklmn', # score = 14 + 2 + 1
'AB12efghijklm', # score = 13 + 2 + 2 u'AB12efghijklm', # score = 13 + 2 + 2
'AB12+fghijkl', # score = 12 + 2 + 2 + 1 u'AB12+fghijkl', # score = 12 + 2 + 2 + 1
'AB12+-ghijk', # score = 11 + 2 + 2 + 2 u'AB12+-ghijk', # score = 11 + 2 + 2 + 2
'AB12+-*hij', # score = 10 + 2 + 2 + 3 u'AB12+-*hij', # score = 10 + 2 + 2 + 3
'AB12+-*hi/', # score = 10 + 2 + 2 + 3 u'AB12+-*hi/', # score = 10 + 2 + 2 + 3
'AB12+-*hi3', # score = 10 + 2 + 2 + 3 u'AB12+-*hi3', # score = 10 + 2 + 2 + 3
'AB12+-*hiC', # score = 10 + 2 + 2 + 3 u'AB12+-*hiC', # score = 10 + 2 + 2 + 3
'abcbbbgbbjklmnopqr', # score = 17 u'abcbbbgbbjklmnopqr', # score = 17
] ]
validator = PasswordScoreValidator(min_classes=0) validator = PasswordScoreValidator(min_classes=0)
@@ -31,19 +31,19 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
except ValidationError as e: except ValidationError as e:
self.assertEqual('too_little_score', e.code) self.assertEqual('too_little_score', e.code)
else: else:
self.fail('%s: no validation error was raised' % password) self.fail(u'%s: no validation error was raised' % password)
def test_too_few_classes(self): def test_too_few_classes(self):
passwords = [ passwords = [
'', # classes = 0 u'', # classes = 0
'abcdefgh', # classes = 1 u'abcdefgh', # classes = 1
'abcdef+-', # classes = 2 u'abcdef+-', # classes = 2
'abcd12gh', # classes = 2 u'abcd12gh', # classes = 2
'abcd12-+', # classes = 3 u'abcd12-+', # classes = 3
'abCDefgh', # classes = 2 u'abCDefgh', # classes = 2
'abCDef+-', # classes = 3 u'abCDef+-', # classes = 3
'abCD12gh', # classes = 3 u'abCD12gh', # classes = 3
'ABCD12-+', # classes = 3 u'ABCD12-+', # classes = 3
] ]
validator = PasswordScoreValidator(min_score=0, min_classes=4) validator = PasswordScoreValidator(min_score=0, min_classes=4)
@@ -54,20 +54,20 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
except ValidationError as e: except ValidationError as e:
self.assertEqual('too_few_classes', e.code) self.assertEqual('too_few_classes', e.code)
else: else:
self.fail('%s: no validation error was raised' % password) self.fail(u'%s: no validation error was raised' % password)
def test_valid(self): def test_valid(self):
passwords = [ passwords = [
'Abcdefghijklmnopq', u'Abcdefghijklmnopq',
'ABcdefghijklmnop', u'ABcdefghijklmnop',
'AB1defghijklmno', u'AB1defghijklmno',
'AB12efghijklmn', u'AB12efghijklmn',
'AB12+fghijklm', u'AB12+fghijklm',
'AB12+-ghijkl', u'AB12+-ghijkl',
'AB12+-*hijk', u'AB12+-*hijk',
'ab1defghijklmnopq', u'ab1defghijklmnopq',
'abcd+fghijklmnopq', u'abcd+fghijklmnopq',
'AB1CDEFGHIJKLMNOPQ', u'AB1CDEFGHIJKLMNOPQ',
] ]
validator = PasswordScoreValidator() validator = PasswordScoreValidator()
@@ -79,6 +79,59 @@ class PasswordScoreValidatorTestCase(SimpleTestCase):
self.fail(e) self.fail(e)
class CustomWordlistPasswordValidatorTestCase(SimpleTestCase):
def test_invalid(self):
invalid_passwords = [
(u'passwort', [
u'The password must not contain the word \'passwort\'',
]),
(u'abcdDaVefgh', [
u'The password must not contain the word \'dav\'',
]),
(u'abcdsektIonefgh', [
u'The password must not contain the word \'sektion\'',
]),
(u'alpen12verein34KArlsruhE berge', [
u'The password must not contain the word \'karlsruhe\'',
u'The password must not contain the word \'berge\'',
]),
(u'heinzel@alpenverein-karlsruhe.de', [
u'The password must not contain the word \'heinzel\'',
u'The password must not contain the word \'alpenverein\'',
u'The password must not contain the word \'karlsruhe\'',
]),
]
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): class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
def setUp(self): def setUp(self):
super(CharacterClassPasswordValidatorTestCase, self).setUp() super(CharacterClassPasswordValidatorTestCase, self).setUp()
@@ -86,72 +139,75 @@ class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
def test_invalid(self): def test_invalid(self):
invalid_passwords = [ invalid_passwords = [
('', [ (u'', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('A+-', [ (u'A+-', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
]), ]),
('1234567890*', [ (u'1234567890*', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('34*/()', [ (u'34*/()', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
]), ]),
('AA', [ (u'AA', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('CD0.,', [ (u'CD0.,', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
]), ]),
('EF56', [ (u'EF56', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('8GH?!8', [ (u'8GH?!8', [
u'The password must contain at least 2 characters from a-z', u'The password must contain at least 2 characters from a-z',
]), ]),
('bbX', [ (u'bbX', [
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('$cd%', [ (u'$cd%', [
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
]), ]),
('ef90', [ (u'ef90', [
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('1g=h3~', [ (u'1g=h3~', [
u'The password must contain at least 2 characters from A-Z', u'The password must contain at least 2 characters from A-Z',
]), ]),
('Gi&jH', [ (u'Gi&jH', [
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
('IkK:i;', [ (u'IkK:i;', [
u'The password must contain at least 2 digits from 0-9', u'The password must contain at least 2 digits from 0-9',
]), ]),
('mKn4L8', [ (u'mKn4L8', [
u'The password must contain at least 2 non alpha numeric characters', u'The password must contain at least 2 non alpha numeric characters',
]), ]),
] ]
validator = self.validator
for password, expected_errors in invalid_passwords: for password, expected_errors in invalid_passwords:
try: try:
self.validator.validate(password) validator.validate(password)
except ValidationError as e: except ValidationError as e:
errors = e.messages errors = e.messages
for expected_error in expected_errors: for expected_error in expected_errors:
@@ -159,12 +215,13 @@ class CharacterClassPasswordValidatorTestCase(SimpleTestCase):
for error in errors: for error in errors:
self.assertIn(error, expected_errors) self.assertIn(error, expected_errors)
else: else:
self.fail('%s: no validation error was raised' % password) self.fail(u'%s: no validation error was raised' % password)
def test_valid(self): def test_valid(self):
valid_passwords = ['abCD12+-'] valid_passwords = [u'abCD12+-']
validator = self.validator
for password in valid_passwords: for password in valid_passwords:
try: try:
self.validator.validate(password) validator.validate(password)
except ValidationError as e: except ValidationError as e:
self.fail(e) self.fail(e)

View File

@@ -4,69 +4,6 @@ from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
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):
errors.append(ValidationError(_(u'The password must contain at least %(min_lower)d characters from a-z'),
code='too_few_lower_characters',
params={'min_lower': self.minimum_lower}))
if not self._is_enough_upper(password):
errors.append(ValidationError(_(u'The password must contain at least %(min_upper)d characters from A-Z'),
code='too_few_upper_characters',
params={'min_upper': self.minimum_upper}))
if not self._is_enough_digits(password):
errors.append(ValidationError(_(u'The password must contain at least %(min_digits)d digits from 0-9'),
code='too_few_digits',
params={'min_digits': self.minimum_digits}))
if not self._is_enough_others(password):
errors.append(ValidationError(_(u'The password must contain at least %(min_others)d'
u' non alpha numeric characters'),
code='too_few_other_characters',
params={'min_others': self.minimum_others}))
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
class PasswordScoreValidator(object): class PasswordScoreValidator(object):
def _get_score(self, password, user=None): def _get_score(self, password, user=None):
score = 0 score = 0
@@ -159,3 +96,105 @@ class PasswordScoreValidator(object):
text += _(u'Also the password must contain characters from %d different character classes' text += _(u'Also the password must contain characters from %d different character classes'
u' (i.e. lower, upper, digits, others).') % self.min_classes u' (i.e. lower, upper, digits, others).') % self.min_classes
return text 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'The password must not contain the word \'%(word)s\''),
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'The password must contain at least %(min_lower)d characters from a-z'),
code='too_few_lower_characters',
params={'min_lower': self.minimum_lower})
errors.append(error)
if not self._is_enough_upper(password):
error = ValidationError(_(u'The password must contain at least %(min_upper)d characters from A-Z'),
code='too_few_upper_characters',
params={'min_upper': self.minimum_upper})
errors.append(error)
if not self._is_enough_digits(password):
error = ValidationError(_(u'The password must contain at least %(min_digits)d digits from 0-9'),
code='too_few_digits',
params={'min_digits': self.minimum_digits})
errors.append(error)
if not self._is_enough_others(password):
error = ValidationError(_(u'The password must contain at least %(min_others)d'
u' non alpha numeric characters'),
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