Merge with master #39

Merged
heinzel merged 61 commits from master into production 2020-12-22 19:45:12 +01:00
2 changed files with 331 additions and 0 deletions
Showing only changes of commit 0762876ec5 - Show all commits

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase
from ..validators import PasswordScoreValidator, CharacterClassPasswordValidator
class PasswordScoreValidatorTestCase(SimpleTestCase):
def test_too_little_score(self):
passwords = [
'', # score = 0
'abcdefghijklmnopq', # score = 17
'Abcdefghijklmnop', # score = 16 + 1
'ABcdefghijklmno', # score = 15 + 2
'AB1defghijklmn', # score = 14 + 2 + 1
'AB12efghijklm', # score = 13 + 2 + 2
'AB12+fghijkl', # score = 12 + 2 + 2 + 1
'AB12+-ghijk', # score = 11 + 2 + 2 + 2
'AB12+-*hij', # score = 10 + 2 + 2 + 3
'AB12+-*hi/', # score = 10 + 2 + 2 + 3
'AB12+-*hi3', # score = 10 + 2 + 2 + 3
'AB12+-*hiC', # score = 10 + 2 + 2 + 3
'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('%s: no validation error was raised' % password)
def test_too_few_classes(self):
passwords = [
'', # classes = 0
'abcdefgh', # classes = 1
'abcdef+-', # classes = 2
'abcd12gh', # classes = 2
'abcd12-+', # classes = 3
'abCDefgh', # classes = 2
'abCDef+-', # classes = 3
'abCD12gh', # classes = 3
'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('%s: no validation error was raised' % password)
def test_valid(self):
passwords = [
'Abcdefghijklmnopq',
'ABcdefghijklmnop',
'AB1defghijklmno',
'AB12efghijklmn',
'AB12+fghijklm',
'AB12+-ghijkl',
'AB12+-*hijk',
'ab1defghijklmnopq',
'abcd+fghijklmnopq',
'AB1CDEFGHIJKLMNOPQ',
]
validator = PasswordScoreValidator()
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'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 non alpha numeric characters',
]),
('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 digits from 0-9',
]),
('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 non alpha numeric characters',
]),
('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',
]),
('AA', [
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 non alpha numeric characters',
]),
('CD0.,', [
u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 digits from 0-9',
]),
('EF56', [
u'The password must contain at least 2 characters from a-z',
u'The password must contain at least 2 non alpha numeric characters',
]),
('8GH?!8', [
u'The password must contain at least 2 characters from a-z',
]),
('bbX', [
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 non alpha numeric characters',
]),
('$cd%', [
u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 digits from 0-9',
]),
('ef90', [
u'The password must contain at least 2 characters from A-Z',
u'The password must contain at least 2 non alpha numeric characters',
]),
('1g=h3~', [
u'The password must contain at least 2 characters from A-Z',
]),
('Gi&jH', [
u'The password must contain at least 2 digits from 0-9',
u'The password must contain at least 2 non alpha numeric characters',
]),
('IkK:i;', [
u'The password must contain at least 2 digits from 0-9',
]),
('mKn4L8', [
u'The password must contain at least 2 non alpha numeric characters',
]),
]
for password, expected_errors in invalid_passwords:
try:
self.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('%s: no validation error was raised' % password)
def test_valid(self):
valid_passwords = ['abCD12+-']
for password in valid_passwords:
try:
self.validator.validate(password)
except ValidationError as e:
self.fail(e)

161
dav_auth/validators.py Normal file
View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
import re
from django.core.exceptions import ValidationError
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):
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'The password must contain characters from at least %(min_classes)d'
u' different character classes (i.e. lower, upper, digits, others).'),
code='too_few_classes',
params={'min_classes': self.min_classes})
if score < self.min_score:
raise ValidationError(_(u'The password is too simple. Use more characters'
u' and maybe use more different character classes'
u' (i.e. lower, upper, digits, others).'),
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