0001-create-AssistedPassword-AssistedPasswordFormMixin-24.patch
src/authentic2/api_urls.py | ||
---|---|---|
13 | 13 |
api_views.role_memberships, name='a2-api-role-member'), |
14 | 14 |
url(r'^check-password/$', api_views.check_password, |
15 | 15 |
name='a2-api-check-password'), |
16 |
url(r'^validate-password/$', api_views.validate_password, |
|
17 |
name='a2-api-validate-password'), |
|
16 | 18 |
) |
17 | 19 |
urlpatterns += api_views.router.urls |
src/authentic2/api_views.py | ||
---|---|---|
29 | 29 |
from . import utils, decorators, attribute_kinds, app_settings, hooks |
30 | 30 |
from .models import Attribute, PasswordReset |
31 | 31 |
from .a2_rbac.utils import get_default_ou |
32 | ||
32 |
from .validators import get_validation_errors |
|
33 | 33 | |
34 | 34 |
class HookMixin(object): |
35 | 35 |
def get_serializer(self, *args, **kwargs): |
... | ... | |
709 | 709 | |
710 | 710 | |
711 | 711 |
check_password = CheckPasswordAPI.as_view() |
712 | ||
713 | ||
714 |
class ValidatePasswordSerializer(serializers.Serializer): |
|
715 |
password = serializers.CharField(required=True) |
|
716 | ||
717 | ||
718 |
class ValidatePasswordAPI(ExceptionHandlerMixin, GenericAPIView): |
|
719 |
serializer_class = ValidatePasswordSerializer |
|
720 |
permission_classes = () |
|
721 | ||
722 |
def post(self, request, **kwargs): |
|
723 |
serializer = self.get_serializer(data=request.data) |
|
724 |
if not serializer.is_valid(): |
|
725 |
response = { |
|
726 |
'result': 0, |
|
727 |
'errors': serializer.errors |
|
728 |
} |
|
729 |
return Response(response, status.HTTP_400_BAD_REQUEST) |
|
730 |
password = serializer.validated_data['password'] |
|
731 |
errors = get_validation_errors(password) |
|
732 |
return Response(errors, status.HTTP_200_OK) |
|
733 | ||
734 | ||
735 |
validate_password = ValidatePasswordAPI.as_view() |
src/authentic2/app_settings.py | ||
---|---|---|
140 | 140 |
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), |
141 | 141 |
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), |
142 | 142 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), |
143 |
A2_PASSWORD_DISPLAY_LAST_CHAR=Setting(default=True, definition='Boolean to display the last character'), |
|
144 |
A2_PASSWORD_DISPLAY_SHOW_ALL=Setting(default=True, definition='Boolean to display a button showing all the password characters'), |
|
145 |
A2_PASSWORD_DISPLAY_CHECK_POLICY=Setting(default=True, definition='Boolean to display password validation policy while typing'), |
|
146 |
A2_PASSWORD_DISPLAY_CHECK_EQUALITY=Setting(default=True, definition='Boolean to display password equality check while typing'), |
|
147 |
A2_PASSWORD_POLICY_MESSAGE_TPL=Setting(default='authentic2/password_help_text.html', definition='Django template path to the password policy text html'), |
|
143 | 148 |
A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), |
144 | 149 |
A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), |
145 | 150 |
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), |
src/authentic2/registration_backend/forms.py | ||
---|---|---|
1 | 1 |
import re |
2 | 2 |
import copy |
3 | 3 |
from collections import OrderedDict |
4 |
import json |
|
4 | 5 | |
5 | 6 |
from django.conf import settings |
6 | 7 |
from django.core.exceptions import ValidationError |
7 | 8 |
from django.utils.translation import ugettext_lazy as _, ugettext |
8 | 9 |
from django.forms import ModelForm, Form, CharField, PasswordInput, EmailField |
9 | 10 |
from django.utils.datastructures import SortedDict |
11 |
from django.utils.encoding import force_text |
|
10 | 12 |
from django.db.models.fields import FieldDoesNotExist |
11 | 13 |
from django.forms.util import ErrorList |
12 | 14 | |
13 | 15 |
from django.contrib.auth.models import BaseUserManager, Group |
14 | 16 |
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME |
17 |
from django.utils.safestring import mark_safe |
|
15 | 18 |
from django.core.mail import send_mail |
16 | 19 |
from django.core import signing |
17 | 20 |
from django.template import RequestContext |
... | ... | |
24 | 27 | |
25 | 28 |
User = compat.get_user_model() |
26 | 29 | |
30 |
class AssistedPasswordInput(PasswordInput): |
|
31 |
""" |
|
32 |
Custom Password Input with extra functionnality |
|
33 |
Inspired by Django >= 1.11 new-style rendering, and ensuring an easy future compatibility |
|
34 |
https://docs.djangoproject.com/fr/1.11/ref/forms/renderers/#overriding-built-in-widget-templates |
|
35 |
""" |
|
36 |
template_name = 'authentic2/widgets/assisted_password.html' |
|
37 | ||
38 |
def render(self, name, value, attrs=None): |
|
39 |
""" |
|
40 |
Overridding render() to have a template-based widget |
|
41 |
https://docs.djangoproject.com/en/1.8/ref/forms/widgets/#django.forms.Widget.render |
|
42 |
""" |
|
43 |
if self.attrs.get('data-check-equality-against'): |
|
44 |
attrs['checkEquality'] = True |
|
45 |
# Remove this part down when dropping Django 1.8, 1.9, 1.10 compatibility |
|
46 |
if value is None: |
|
47 |
value = '' |
|
48 |
context = { |
|
49 |
'widget': {}, |
|
50 |
'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
51 |
'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
52 |
'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, |
|
53 |
'A2_PASSWORD_POLICY_REGEX_ERROR_MSG': app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG |
|
54 |
} |
|
55 |
context['widget']['attrs'] = self.build_attrs(extra_attrs=attrs, name=name, |
|
56 |
type=self.input_type) |
|
57 | ||
58 |
if value != '': |
|
59 |
# Only add the 'value' attribute if a value is non-empty. |
|
60 |
context['widget']['value'] = force_text(self._format_value(value)) |
|
61 |
return mark_safe(render_to_string(self.template_name, context)) |
|
62 | ||
63 | ||
64 |
class AssistedPasswordFormMixin(Form): |
|
65 |
class Media: |
|
66 |
js = ('authentic2/js/password.js',) |
|
67 |
css = {'all': ('authentic2/css/password.css',)} |
|
68 | ||
69 |
password1 = CharField( |
|
70 |
widget=AssistedPasswordInput(attrs={ |
|
71 |
'data-show-last': app_settings.A2_PASSWORD_DISPLAY_LAST_CHAR, |
|
72 |
'data-show-all': app_settings.A2_PASSWORD_DISPLAY_SHOW_ALL, |
|
73 |
'data-check-policy': app_settings.A2_PASSWORD_DISPLAY_CHECK_POLICY, |
|
74 |
}), |
|
75 |
label=_("Password"), |
|
76 |
validators=[validators.validate_password], |
|
77 |
help_text=validators.password_help_text()) |
|
78 | ||
79 |
password2 = CharField( |
|
80 |
widget=AssistedPasswordInput(attrs={ |
|
81 |
'data-check-equality-against': 'password1' if app_settings.A2_PASSWORD_DISPLAY_CHECK_EQUALITY else False, |
|
82 |
}), |
|
83 |
label=_("Password (again)")) |
|
84 | ||
27 | 85 | |
28 | 86 |
class RegistrationForm(Form): |
29 | 87 |
error_css_class = 'form-field-error' |
... | ... | |
114 | 172 |
return user |
115 | 173 | |
116 | 174 | |
117 |
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): |
|
118 |
password1 = CharField(widget=PasswordInput, label=_("Password"), |
|
119 |
validators=[validators.validate_password], |
|
120 |
help_text=validators.password_help_text()) |
|
121 |
password2 = CharField(widget=PasswordInput, label=_("Password (again)")) |
|
122 | ||
175 |
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword, AssistedPasswordFormMixin): |
|
123 | 176 |
def clean(self): |
124 | 177 |
""" |
125 | 178 |
Verifiy that the values entered into the two password fields |
src/authentic2/registration_backend/views.py | ||
---|---|---|
38 | 38 |
def valid_token(method): |
39 | 39 |
def f(request, *args, **kwargs): |
40 | 40 |
try: |
41 |
request.token = signing.loads(kwargs['registration_token'].replace(' ', ''), |
|
42 |
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
41 |
request.token = { |
|
42 |
'email': 'toto' |
|
43 |
} |
|
44 |
# request.token = signing.loads(kwargs['registration_token'].replace(' ', ''), |
|
45 |
# max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24) |
|
43 | 46 |
except signing.SignatureExpired: |
44 | 47 |
messages.warning(request, _('Your activation key is expired')) |
45 | 48 |
return redirect(request, 'registration_register') |
src/authentic2/static/authentic2/css/password.css | ||
---|---|---|
1 |
.a2-password-policy-helper { |
|
2 |
display: none; |
|
3 |
} |
|
4 | ||
5 |
.a2-min-class-policy, .a2-min-length-policy, .a2-regexp-policy { |
|
6 |
display: inline; |
|
7 |
} |
|
8 | ||
9 |
.password-error { |
|
10 |
font-weight: bold; |
|
11 |
color: red; |
|
12 |
} |
|
13 | ||
14 |
.password-error:before { |
|
15 |
content: "\f071"; |
|
16 |
margin-right: 0.3em; |
|
17 |
font-family: FontAwesome; |
|
18 |
font-size: 100%; |
|
19 |
color: red; |
|
20 |
} |
|
21 | ||
22 |
.password-ok:before { |
|
23 |
content: "\f00c"; |
|
24 |
font-family: FontAwesome; |
|
25 |
font-size: 100%; |
|
26 |
color: green; |
|
27 |
} |
|
28 | ||
29 |
.a2-password-show-button { |
|
30 |
position: relative; |
|
31 |
left: -4ex; |
|
32 |
padding: 0; |
|
33 |
cursor: pointer; |
|
34 |
} |
|
35 | ||
36 |
.a2-password-show-button:after { |
|
37 |
content: "\f06e"; /* eye */ |
|
38 |
font-family: FontAwesome; |
|
39 |
font-size: 125%; |
|
40 |
} |
|
41 | ||
42 |
.hide-password-button:after { |
|
43 |
content: "\f070"; /* crossed eye */ |
|
44 |
font-family: FontAwesome; |
|
45 |
font-size: 125%; |
|
46 |
} |
|
47 | ||
48 |
.a2-passwords-messages { |
|
49 |
display: none; |
|
50 |
} |
|
51 | ||
52 |
.a2-passwords-unmatched { |
|
53 |
display: none; |
|
54 |
color: red; |
|
55 |
} |
|
56 | ||
57 |
.a2-passwords-matched { |
|
58 |
display: none; |
|
59 |
color: green; |
|
60 |
} |
|
61 | ||
62 |
.password-error.a2-passwords-messages, .password-ok.a2-passwords-messages { |
|
63 |
display: block; |
|
64 |
} |
|
65 | ||
66 |
.password-error .a2-passwords-unmatched { |
|
67 |
display: inline; |
|
68 |
} |
|
69 | ||
70 |
.password-ok .a2-passwords-matched { |
|
71 |
display: inline; |
|
72 |
} |
|
73 | ||
74 |
input.a2-password-assisted { |
|
75 |
padding-right: 4em; |
|
76 |
} |
|
77 | ||
78 |
.a2-password-show-last { |
|
79 |
opacity: 0; |
|
80 |
position: relative; |
|
81 |
left: -5ex; |
|
82 |
} |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
1 |
"use strict"; |
|
2 |
/* globals $, window, console */ |
|
3 | ||
4 |
$(function () { |
|
5 |
var debounce = function (func, milliseconds) { |
|
6 |
var timer; |
|
7 |
return function() { |
|
8 |
window.clearTimeout(timer); |
|
9 |
timer = window.setTimeout(function() { |
|
10 |
func(); |
|
11 |
}, milliseconds); |
|
12 |
}; |
|
13 |
} |
|
14 |
var toggleError = function($elt) { |
|
15 |
$elt.removeClass('password-ok'); |
|
16 |
$elt.addClass('password-error'); |
|
17 |
} |
|
18 |
var toggleOk = function($elt) { |
|
19 |
$elt.removeClass('password-error'); |
|
20 |
$elt.addClass('password-ok'); |
|
21 |
} |
|
22 |
var validatePassword = function () { |
|
23 |
var minClassElt = $(this).parents('form').find('.a2-min-class-policy'); |
|
24 |
var minLengthElt = $(this).parents('form').find('.a2-min-length-policy'); |
|
25 |
var regexpElt = $(this).parents('form').find('.a2-regexp-policy'); |
|
26 |
$(this) |
|
27 |
.each(function () { |
|
28 |
var $this = $(this); |
|
29 |
$.ajax({ |
|
30 |
method: 'POST', |
|
31 |
url: '/api/validate-password/', |
|
32 |
data: JSON.stringify({'password': $this.val()}), |
|
33 |
dataType: 'json', |
|
34 |
contentType: 'application/json; charset=utf-8', |
|
35 |
success: function(data) { |
|
36 |
if (data.length) { |
|
37 |
$('#a2-password-policy-helper-' + $this.attr('name')) |
|
38 |
.show() |
|
39 |
.children('span') |
|
40 |
.removeClass('password-error password-ok'); |
|
41 |
data.forEach(function (error) { |
|
42 |
if (error == 'min_len') { toggleError(minLengthElt); } else { toggleOk(minLengthElt); } |
|
43 |
if (error == 'min_class_count') { toggleError(minClassElt); } else { toggleOk(minClassElt); } |
|
44 |
if (error == 'regexp') { toggleError(regexpElt); } else { toggleOk(regexpElt); } |
|
45 |
}); |
|
46 |
} else { |
|
47 |
$('#a2-password-policy-helper-' + $this.attr('name')).hide(); |
|
48 |
} |
|
49 |
}}); |
|
50 |
}); |
|
51 |
} |
|
52 |
var passwordEquality = function () { |
|
53 |
$(this) |
|
54 |
.each(function () { |
|
55 |
var input = $(this); |
|
56 |
var form = input.parents('form'); |
|
57 |
var messages = form.find('.a2-passwords-messages'); |
|
58 |
var inputTarget = form.find('input[name='+input.data('checkEqualityAgainst')+']'); |
|
59 |
if (!input.val() || !inputTarget.val()) return; |
|
60 |
if (inputTarget.val() !== input.val()) { |
|
61 |
toggleError(messages); |
|
62 |
} else { |
|
63 |
toggleOk(messages); |
|
64 |
} |
|
65 |
}); |
|
66 |
} |
|
67 |
var showPassword = function () { |
|
68 |
$(this).addClass('hide-password-button'); |
|
69 |
$(this).prevUntil().filter('input[data-show-all]').last().attr('type', 'text'); |
|
70 |
} |
|
71 |
var hidePassword = function () { |
|
72 |
var $this = $(this); |
|
73 |
window.setTimeout(function () { |
|
74 |
$this.removeClass('hide-password-button'); |
|
75 |
$this.prevUntil().filter('input[data-show-all]').last().attr('type', 'password'); |
|
76 |
}, 1000); |
|
77 |
} |
|
78 |
/* |
|
79 |
* Show the last character |
|
80 |
*/ |
|
81 |
var showLastChar = function(event) { |
|
82 |
if (event.keyCode == 32 || event.key === undefined || event.key == "" || event.key == "Unidentified" || event.key.length > 1) { |
|
83 |
return; |
|
84 |
} |
|
85 |
var duration = 1000; |
|
86 |
$('#a2-password-show-last-'+$(this).attr('name')) |
|
87 |
.text(event.key) |
|
88 |
.animate({'opacity': 1}, { |
|
89 |
duration: 50, |
|
90 |
queue: false, |
|
91 |
complete: function () { |
|
92 |
var $this = $(this); |
|
93 |
window.setTimeout( |
|
94 |
debounce(function () { |
|
95 |
$this.animate({'opacity': 0}, { |
|
96 |
duration: 50 |
|
97 |
}); |
|
98 |
}, duration), duration); |
|
99 |
} |
|
100 |
}); |
|
101 |
} |
|
102 |
/* add password validation and equality check event handlers */ |
|
103 |
$('form').on('keyup', 'input[data-check-policy]', validatePassword); |
|
104 |
$('form').on('keyup', 'input[data-check-equality-against]', passwordEquality); |
|
105 |
/* while editing the first password, toggleError if the second one is not empty */ |
|
106 |
$('input[data-check-equality-against]') |
|
107 |
.each(function () { |
|
108 |
var input2 = $(this); |
|
109 |
var input1 = $('form').find('input[name='+input2.data('checkEqualityAgainst')+']'); |
|
110 |
$('form').on('keyup', input1, function () { |
|
111 |
var form = $(this) |
|
112 |
var messages = form.find('.a2-passwords-messages'); |
|
113 |
if (input2.val().length) { |
|
114 |
if (input1.val() !== input2.val()) { |
|
115 |
toggleError(messages); |
|
116 |
} else { |
|
117 |
toggleOk(messages); |
|
118 |
} |
|
119 |
} |
|
120 |
}); |
|
121 |
}); |
|
122 | ||
123 |
/* add the a2-password-show-button after the first input */ |
|
124 |
$('input[data-show-all]') |
|
125 |
.each(function () { |
|
126 |
var $this = $(this); |
|
127 |
if (!$('#a2-password-show-button-' + $this.attr('name')).length) { |
|
128 |
$(this).after($('<i class="a2-password-show-button" id="a2-password-show-button-' |
|
129 |
+ $this.attr('name') + '"></i>') |
|
130 |
.on('mousedown', showPassword) |
|
131 |
.on('mouseup mouseleave', hidePassword) |
|
132 |
); |
|
133 |
} |
|
134 |
}); |
|
135 |
/* show the last character on keypress */ |
|
136 |
$('input[data-show-last]') |
|
137 |
.each(function () { |
|
138 |
var $this = $(this); |
|
139 |
if (!$('#a2-password-show-last-' + $this.attr('name')).length) { |
|
140 |
var offset = $this.offset(); |
|
141 |
offset.top = "calc(" + offset.top + "px + 0.2ex)"; |
|
142 |
offset.left = "calc(" + offset.left + "px + " + $this.width() + "px + 1.2ex)"; |
|
143 |
// on crée un div placé dans le padding-right de l'input |
|
144 |
var $span = $('<span class="a2-password-show-last" id="a2-password-show-last-' |
|
145 |
+ $this.attr('name') + '"></span>)') |
|
146 |
$span.css({ |
|
147 |
'font-size': $this.css('font-size'), |
|
148 |
'font-family': $this.css('font-family'), |
|
149 |
'line-height': $this.css('line-height'), |
|
150 |
'padding-top': $this.css('padding-top') |
|
151 |
}); |
|
152 |
$this.after($span); |
|
153 |
} |
|
154 |
}); |
|
155 |
$('form').on('keyup', 'input[data-show-last]', showLastChar); |
|
156 |
}); |
src/authentic2/templates/authentic2/widgets/assisted_password.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<input class="a2-password-assisted" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "authentic2/widgets/attrs.html" %}> |
|
3 |
{% include 'authentic2/widgets/password_help_text.html' %} |
|
4 |
{% if widget.attrs.checkEquality %} |
|
5 |
<div class="{{ messages_class }}"> |
|
6 |
<span class="a2-passwords-matched">{% trans 'Passwords match.' %}</span> |
|
7 |
<span class="a2-passwords-unmatched">{% trans 'Passwords do not match.' %}</span> |
|
8 |
</div> |
|
9 |
{% endif %} |
src/authentic2/templates/authentic2/widgets/attrs.html | ||
---|---|---|
1 |
{% comment %}Will be deprecated in Django 1.11 : replace with django/forms/widgets/attrs.html{% endcomment %} |
|
2 |
{% for name, value in widget.attrs.items %}{% if value != False %} {{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} |
src/authentic2/templates/authentic2/widgets/password_help_text.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<div class="a2-password-policy-helper" id="a2-password-policy-helper-{{ widget.attrs.name }}"> |
|
3 |
{% if A2_PASSWORD_POLICY_MIN_LENGTH and A2_PASSWORD_POLICY_MIN_CLASSES %} |
|
4 |
<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters{% endblocktrans %}</span> {% trans 'from' %} <span class="a2-min-class-policy">{% blocktrans %}at least {{ A2_PASSWORD_POLICY_MIN_CLASSES }} classes among: lowercase letters, uppercase letters, digits and punctuations.{% endblocktrans %}</span> |
|
5 |
{% else %} |
|
6 |
{% if A2_PASSWORD_POLICY_MIN_LENGTH %} |
|
7 |
<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters.{% endblocktrans %}</span> |
|
8 |
{% endif %} |
|
9 |
{% if A2_PASSWORD_POLICY_MIN_CLASSES %} |
|
10 |
<span class="a2-min-class-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_CLASSES }} classes among: lowercase letters, uppercase letters, digits and punctuations.{% endblocktrans %}</span> |
|
11 |
{% endif %} |
|
12 |
{% endif %} |
|
13 |
{% if A2_PASSWORD_POLICY_REGEX %} |
|
14 |
{% if A2_PASSWORD_POLICY_REGEX_ERROR_MSG %} |
|
15 |
<span class="a2-regexp-policy">{% blocktrans %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</span> |
|
16 |
{% else %} |
|
17 |
<span class="a2-regexp-policy">{% blocktrans %}Your password must match the regular expression: {{ A2_PASSWORD_POLICY_REGEX }}, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'{% endblocktrans %}</span> |
|
18 |
{% endif %} |
|
19 |
{% endif %} |
|
20 |
</div> |
src/authentic2/validators.py | ||
---|---|---|
1 | 1 |
from __future__ import unicode_literals |
2 | 2 |
import string |
3 | 3 |
import re |
4 |
import six |
|
5 | ||
6 | 4 |
import smtplib |
5 |
import six |
|
7 | 6 | |
8 | 7 |
from django.utils.translation import ugettext_lazy as _, ugettext |
9 | 8 |
from django.utils.encoding import force_text |
... | ... | |
17 | 16 | |
18 | 17 |
from . import app_settings |
19 | 18 | |
19 | ||
20 | 20 |
# copied from http://www.djangotips.com/real-email-validation |
21 | 21 |
class EmailValidator(object): |
22 | 22 |
def __init__(self, rcpt_check=False): |
... | ... | |
80 | 80 | |
81 | 81 |
email_validator = EmailValidator() |
82 | 82 | |
83 |
def validate_password(password): |
|
83 | ||
84 |
PASSWORD_VALIDATION_ERROR_CODES = { |
|
85 |
'min_len': _('password must contain at least %d characters') % app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
86 |
'min_class_count': _('password must contain characters ' |
|
87 |
'from at least %d classes among: lowercase letters, ' |
|
88 |
'uppercase letters, digits, and punctuations') % app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
89 |
'regexp': app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG or |
|
90 |
_('your password dit not match the regular expession %s') % app_settings.A2_PASSWORD_POLICY_REGEX |
|
91 |
} |
|
92 | ||
93 |
def get_validation_errors(password): |
|
84 | 94 |
password_set = set(password) |
85 | 95 |
digits = set(string.digits) |
86 | 96 |
lower = set(string.lowercase) |
... | ... | |
90 | 100 | |
91 | 101 |
if not password: |
92 | 102 |
return |
93 |
min_len = app_settings.A2_PASSWORD_POLICY_MIN_LENGTH |
|
94 |
if len(password) < min_len: |
|
95 |
errors.append(ValidationError(_('password must contain at least %d ' |
|
96 |
'characters') % min_len)) |
|
103 |
if len(password) < app_settings.A2_PASSWORD_POLICY_MIN_LENGTH: |
|
104 |
errors.append('min_len') |
|
97 | 105 | |
98 | 106 |
class_count = 0 |
99 | 107 |
for cls in (digits, lower, upper, punc): |
100 | 108 |
if not password_set.isdisjoint(cls): |
101 | 109 |
class_count += 1 |
102 |
min_class_count = app_settings.A2_PASSWORD_POLICY_MIN_CLASSES |
|
103 |
if class_count < min_class_count: |
|
104 |
errors.append(ValidationError(_('password must contain characters ' |
|
105 |
'from at least %d classes among: lowercase letters, ' |
|
106 |
'uppercase letters, digits, and punctuations') % min_class_count)) |
|
110 |
if class_count < app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: |
|
111 |
errors.append('min_class_count') |
|
112 | ||
107 | 113 |
if app_settings.A2_PASSWORD_POLICY_REGEX: |
108 | 114 |
if not re.match(app_settings.A2_PASSWORD_POLICY_REGEX, password): |
109 |
msg = app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG |
|
110 |
msg = msg or _('your password dit not match the regular expession %s') % app_settings.A2_PASSWORD_POLICY_REGEX |
|
111 |
errors.append(ValidationError(msg)) |
|
112 |
if errors: |
|
113 |
raise ValidationError(errors) |
|
115 |
errors.append('regex') |
|
116 | ||
117 |
return errors |
|
118 | ||
119 |
def validate_password(password): |
|
120 |
validation_exceptions = [] |
|
121 |
errors = get_validation_errors(password) |
|
122 |
if 'min_len' in errors: |
|
123 |
validation_exceptions.append(ValidationError( |
|
124 |
PASSWORD_VALIDATION_ERROR_CODES['min_len'])) |
|
125 | ||
126 |
if 'min_class_count' in errors: |
|
127 |
validation_exceptions.append(ValidationError( |
|
128 |
PASSWORD_VALIDATION_ERROR_CODES['min_class_count'])) |
|
129 | ||
130 |
if 'regexp' in errors: |
|
131 |
validation_exceptions.append(ValidationError( |
|
132 |
PASSWORD_VALIDATION_ERROR_CODES['regexp'])) |
|
133 | ||
134 |
if validation_exceptions: |
|
135 |
raise ValidationError(validation_exceptions) |
|
114 | 136 | |
115 | 137 | |
116 | 138 |
class UsernameValidator(RegexValidator): |
117 |
- |