144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
# authentic2 - versatile identity manager
|
|
# Copyright (C) 2010-2019 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import string
|
|
import random
|
|
import re
|
|
import abc
|
|
|
|
from django.utils.translation import ugettext as _
|
|
from django.utils.module_loading import import_string
|
|
from django.utils.functional import lazy
|
|
from django.utils import six
|
|
from django.core.exceptions import ValidationError
|
|
|
|
|
|
from . import app_settings
|
|
|
|
|
|
def generate_password():
|
|
'''Generate a password that validates current password policy.
|
|
|
|
Beware that A2_PASSWORD_POLICY_REGEX cannot be validated.
|
|
'''
|
|
digits = string.digits
|
|
lower = string.ascii_lowercase
|
|
upper = string.ascii_uppercase
|
|
punc = string.punctuation
|
|
|
|
min_len = max(app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, 8)
|
|
min_class_count = max(app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, 3)
|
|
new_password = []
|
|
|
|
generator = random.SystemRandom()
|
|
while len(new_password) < min_len:
|
|
for cls in (digits, lower, upper, punc)[:min_class_count]:
|
|
new_password.append(generator.choice(cls))
|
|
generator.shuffle(new_password)
|
|
return ''.join(new_password)
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class PasswordChecker(object):
|
|
class Check(object):
|
|
def __init__(self, label, result):
|
|
self.label = label
|
|
self.result = result
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def __call__(self, password, **kwargs):
|
|
'''Return an iterable of Check objects giving the list of checks and
|
|
their result.'''
|
|
return []
|
|
|
|
|
|
class DefaultPasswordChecker(PasswordChecker):
|
|
@property
|
|
def min_length(self):
|
|
return app_settings.A2_PASSWORD_POLICY_MIN_LENGTH
|
|
|
|
@property
|
|
def at_least_one_lowercase(self):
|
|
return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 0
|
|
|
|
@property
|
|
def at_least_one_digit(self):
|
|
return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 1
|
|
|
|
@property
|
|
def at_least_one_uppercase(self):
|
|
return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 2
|
|
|
|
@property
|
|
def regexp(self):
|
|
return app_settings.A2_PASSWORD_POLICY_REGEX
|
|
|
|
@property
|
|
def regexp_label(self):
|
|
return app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG
|
|
|
|
def __call__(self, password, **kwargs):
|
|
if self.min_length:
|
|
yield self.Check(
|
|
result=len(password) >= self.min_length,
|
|
label=_('%s characters') % self.min_length)
|
|
|
|
if self.at_least_one_lowercase:
|
|
yield self.Check(
|
|
result=any(c.islower() for c in password),
|
|
label=_('1 lowercase letter'))
|
|
|
|
if self.at_least_one_digit:
|
|
yield self.Check(
|
|
result=any(c.isdigit() for c in password),
|
|
label=_('1 digit'))
|
|
|
|
if self.at_least_one_uppercase:
|
|
yield self.Check(
|
|
result=any(c.isupper() for c in password),
|
|
label=_('1 uppercase letter'))
|
|
|
|
if self.regexp and self.regexp_label:
|
|
yield self.Check(
|
|
result=bool(re.match(self.regexp, password)),
|
|
label=self.regexp_label)
|
|
|
|
|
|
def get_password_checker(*args, **kwargs):
|
|
return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs)
|
|
|
|
|
|
def validate_password(password):
|
|
error = password_help_text(password, only_errors=True)
|
|
if error:
|
|
raise ValidationError(_('This password is not accepted.'))
|
|
|
|
|
|
def password_help_text(password='', only_errors=False):
|
|
password_checker = get_password_checker()
|
|
criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)]
|
|
if criteria:
|
|
html_criteria = [u'<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria]
|
|
return _('In order to create a secure password, please use at least: '
|
|
'<span class="a2-password-policy-container">%s</span>') % (''.join(html_criteria))
|
|
else:
|
|
return ''
|
|
|
|
password_help_text = lazy(password_help_text, six.text_type)
|