0001-create-assisted-password-input-widgets-24438.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_view, |
|
17 |
name='a2-api-validate-password'), |
|
16 | 18 |
] |
17 | 19 | |
18 | 20 |
urlpatterns += api_views.router.urls |
src/authentic2/api_views.py | ||
---|---|---|
22 | 22 |
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed |
23 | 23 |
from rest_framework.fields import CreateOnlyDefault |
24 | 24 |
from rest_framework.decorators import list_route, detail_route |
25 |
from rest_framework.authentication import SessionAuthentication, BasicAuthentication |
|
25 | 26 | |
26 | 27 |
from django_filters.rest_framework import FilterSet |
27 | 28 | |
... | ... | |
29 | 30 |
from . import utils, decorators, attribute_kinds, app_settings, hooks |
30 | 31 |
from .models import Attribute, PasswordReset |
31 | 32 |
from .a2_rbac.utils import get_default_ou |
33 |
from .validators import password_validation_rules |
|
32 | 34 | |
33 | 35 | |
34 | 36 |
class HookMixin(object): |
... | ... | |
709 | 711 | |
710 | 712 | |
711 | 713 |
check_password = CheckPasswordAPI.as_view() |
714 | ||
715 | ||
716 |
class CsrfExemptSessionAuthentication(SessionAuthentication): |
|
717 |
def enforce_csrf(self, request): |
|
718 |
return # To not perform the csrf check previously happening |
|
719 | ||
720 | ||
721 |
class ValidatePasswordSerializer(serializers.Serializer): |
|
722 |
password = serializers.CharField(required=True) |
|
723 | ||
724 | ||
725 |
class ValidatePasswordAPI(ExceptionHandlerMixin, GenericAPIView): |
|
726 |
serializer_class = ValidatePasswordSerializer |
|
727 |
permission_classes = (permissions.AllowAny,) |
|
728 |
authentication_classes = (CsrfExemptSessionAuthentication,) |
|
729 | ||
730 |
def post(self, request, **kwargs): |
|
731 |
serializer = self.get_serializer(data=request.data) |
|
732 |
if not serializer.is_valid(): |
|
733 |
response = { |
|
734 |
'result': 0, |
|
735 |
'errors': serializer.errors |
|
736 |
} |
|
737 |
return Response(response, status.HTTP_400_BAD_REQUEST) |
|
738 |
password = serializer.validated_data['password'] |
|
739 |
return Response({ |
|
740 |
'result': 1, |
|
741 |
'validation': password_validation_rules(password), |
|
742 |
}, status.HTTP_200_OK) |
|
743 | ||
744 | ||
745 |
validate_password_view = ValidatePasswordAPI.as_view() |
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 |
... | ... | |
15 | 16 |
from django.core.mail import send_mail |
16 | 17 |
from django.core import signing |
17 | 18 |
from django.template import RequestContext |
18 |
from django.template.loader import render_to_string |
|
19 | 19 |
from django.core.urlresolvers import reverse |
20 | 20 |
from django.core.validators import RegexValidator |
21 | 21 | |
22 |
from .widgets import CheckPasswordInput, NewPasswordInput |
|
22 | 23 |
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks |
23 | 24 |
from authentic2.a2_rbac.models import OrganizationalUnit |
24 | 25 | |
... | ... | |
115 | 116 | |
116 | 117 | |
117 | 118 |
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)")) |
|
119 | ||
120 |
password1 = CharField(widget=NewPasswordInput(), label=_("Password"), |
|
121 |
validators=[validators.validate_password], |
|
122 |
help_text=validators.password_help_text()) |
|
123 |
password2 = CharField(widget=CheckPasswordInput(), label=_("Password (again)")) |
|
122 | 124 | |
123 | 125 |
def clean(self): |
124 | 126 |
""" |
src/authentic2/registration_backend/widgets.py | ||
---|---|---|
1 |
from django.forms import PasswordInput |
|
2 |
from django.template.loader import render_to_string |
|
3 |
from django.utils.encoding import force_text |
|
4 |
from django.utils.safestring import mark_safe |
|
5 | ||
6 |
from .. import app_settings |
|
7 | ||
8 | ||
9 |
class BasePasswordInput(PasswordInput): |
|
10 |
""" |
|
11 |
a password Input with some features to help the user choosing a new password |
|
12 |
Inspired by Django >= 1.11 new-style rendering |
|
13 |
(cf. https://docs.djangoproject.com/fr/1.11/ref/forms/renderers) |
|
14 |
""" |
|
15 |
template_name = 'authentic2/widgets/assisted_password.html' |
|
16 |
features = {} |
|
17 | ||
18 |
class Media: |
|
19 |
js = ('authentic2/js/password.js',) |
|
20 |
css = { |
|
21 |
'all': ('authentic2/css/password.css',) |
|
22 |
} |
|
23 | ||
24 |
def get_context(self, name, value, attrs): |
|
25 |
""" |
|
26 |
Base get_context |
|
27 |
""" |
|
28 |
context = { |
|
29 |
'app_settings': { |
|
30 |
'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
31 |
'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
32 |
'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, |
|
33 |
}, |
|
34 |
'features': self.features |
|
35 |
} |
|
36 |
# attach data-* attributes for password.js to activate events |
|
37 |
attrs.update(dict([('data-%s' % feat.replace('_', '-'), is_active) for feat, is_active in self.features.items()])) |
|
38 | ||
39 |
context['widget'] = { |
|
40 |
'name': name, |
|
41 |
'is_hidden': self.is_hidden, |
|
42 |
'required': self.is_required, |
|
43 |
'template_name': self.template_name, |
|
44 |
'attrs': self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type) |
|
45 |
} |
|
46 |
# Only add the 'value' attribute if a value is non-empty. |
|
47 |
if value is None: |
|
48 |
value = '' |
|
49 |
if value != '': |
|
50 |
context['widget']['value'] = force_text(self._format_value(value)) |
|
51 | ||
52 |
return context |
|
53 | ||
54 |
def render(self, name, value, attrs=None, **kwargs): |
|
55 |
""" |
|
56 |
Override render with a template-based system |
|
57 |
Remove this line when dropping Django 1.8, 1.9, 1.10 compatibility |
|
58 |
""" |
|
59 |
return mark_safe(render_to_string(self.template_name, |
|
60 |
self.get_context(name, value, attrs))) |
|
61 | ||
62 | ||
63 |
class CheckPasswordInput(BasePasswordInput): |
|
64 |
""" |
|
65 |
Password typing assistance widget (eg. password2) |
|
66 |
""" |
|
67 |
features = { |
|
68 |
'check_equality': True, |
|
69 |
'show_all': True, |
|
70 |
'show_last': True, |
|
71 |
} |
|
72 | ||
73 |
def get_context(self, name, value, attrs): |
|
74 |
context = super(CheckPasswordInput, self).get_context( |
|
75 |
name, value, attrs) |
|
76 |
return context |
|
77 | ||
78 | ||
79 |
class NewPasswordInput(CheckPasswordInput): |
|
80 |
""" |
|
81 |
Password creation assistance widget (eg. password1) |
|
82 |
""" |
|
83 |
features = { |
|
84 |
'check_equality': False, |
|
85 |
'show_all': True, |
|
86 |
'show_last': True, |
|
87 |
'check_policy': True, |
|
88 |
} |
|
89 | ||
90 |
def get_context(self, name, value, attrs): |
|
91 |
context = super(NewPasswordInput, self).get_context(name, value, attrs) |
|
92 |
return context |
src/authentic2/static/authentic2/css/password.css | ||
---|---|---|
1 |
/* required in order to position a2-password-show-all and a2-password-show-last */ |
|
2 |
input[type=password].a2-password-assisted { |
|
3 |
padding-right: 60px; |
|
4 |
width: 100%; |
|
5 |
} |
|
6 | ||
7 |
.a2-password-policy-helper { |
|
8 |
display: flex; |
|
9 |
height: auto; |
|
10 |
flex-direction: row; |
|
11 |
flex-wrap: wrap; |
|
12 |
position: relative; |
|
13 |
padding: 0.5rem 1rem; |
|
14 |
} |
|
15 | ||
16 |
/* we don't want helptext when a2-password-policy-helper is here */ |
|
17 |
.a2-password-policy-helper ~ .helptext { |
|
18 |
display: none; |
|
19 |
} |
|
20 | ||
21 |
.a2-min-class-policy, |
|
22 |
.a2-min-length-policy, |
|
23 |
.a2-regexp-policy { |
|
24 |
flex: 1 1 0; |
|
25 |
list-style-position: inside; |
|
26 |
} |
|
27 | ||
28 |
.a2-password-policy-helper > .password-error, |
|
29 |
.a2-password-policy-helper > .password-ok { |
|
30 |
list-style: none; |
|
31 |
} |
|
32 | ||
33 | ||
34 |
.password-error { |
|
35 |
font-weight: normal; |
|
36 |
color: black; |
|
37 |
} |
|
38 | ||
39 |
.password-ok { |
|
40 |
font-weight: normal; |
|
41 |
color: green; |
|
42 |
} |
|
43 | ||
44 |
.password-error:before { |
|
45 |
content: "\f00d"; /* cross icon */ |
|
46 |
margin-right: 0.5rem; |
|
47 |
font-family: FontAwesome; |
|
48 |
font-size: 100%; |
|
49 |
color: red; |
|
50 |
} |
|
51 | ||
52 |
.password-ok:before { |
|
53 |
content: "\f00c"; /* ok icon */ |
|
54 |
margin-right: 0.5rem; |
|
55 |
font-family: FontAwesome; |
|
56 |
font-size: 100%; |
|
57 |
color: green; |
|
58 |
} |
|
59 | ||
60 |
.a2-password-show-last { |
|
61 |
display: inline-block; |
|
62 |
opacity: 0; |
|
63 |
float: right; |
|
64 |
text-align: center; |
|
65 |
position: relative; |
|
66 |
right: 30px; |
|
67 |
top: -4.5ex; |
|
68 |
width: 20px; |
|
69 |
} |
|
70 | ||
71 |
.a2-password-show-button { |
|
72 |
display: inline-block; |
|
73 |
float: right; |
|
74 |
position: relative; |
|
75 |
padding: 0; |
|
76 |
width: 0px; |
|
77 |
right: 10px; |
|
78 |
top: -4ex; |
|
79 |
cursor: pointer; |
|
80 |
} |
|
81 | ||
82 |
.a2-password-show-button:after { |
|
83 |
content: "\f06e"; /* eye */ |
|
84 |
font-family: FontAwesome; |
|
85 |
font-size: 125%; |
|
86 |
} |
|
87 | ||
88 |
.hide-password-button:after { |
|
89 |
content: "\f070"; /* crossed eye */ |
|
90 |
font-family: FontAwesome; |
|
91 |
font-size: 125%; |
|
92 |
} |
|
93 | ||
94 |
.a2-passwords-messages { |
|
95 |
display: none; |
|
96 |
opacity: 0; |
|
97 |
transition: all 0.3s ease; |
|
98 |
} |
|
99 | ||
100 |
.a2-passwords-unmatched { |
|
101 |
display: none; |
|
102 |
} |
|
103 | ||
104 |
.a2-passwords-matched { |
|
105 |
display: none; |
|
106 |
} |
|
107 | ||
108 |
.password-error.a2-passwords-messages, |
|
109 |
.password-ok.a2-passwords-messages { |
|
110 |
display: flex; |
|
111 |
flex-wrap: wrap; |
|
112 |
padding: 0.5rem 1rem; |
|
113 |
opacity: 1; |
|
114 |
} |
|
115 | ||
116 |
.password-error.a2-passwords-messages:before, |
|
117 |
.password-ok.a2-passwords-messages:before { |
|
118 |
display: none; |
|
119 |
} |
|
120 | ||
121 |
.password-error .a2-passwords-unmatched, |
|
122 |
.password-ok .a2-passwords-matched { |
|
123 |
display: list-item; |
|
124 |
list-style: none; |
|
125 |
} |
|
126 | ||
127 |
.password-error .a2-passwords-unmatched:before { |
|
128 |
content: "\f00d"; /* cross icon */ |
|
129 |
margin-right: 0.5rem; |
|
130 |
font-family: FontAwesome; |
|
131 |
font-size: 100%; |
|
132 |
color: red; |
|
133 |
} |
|
134 | ||
135 |
.password-ok .a2-passwords-matched:before { |
|
136 |
content: "\f00c"; /* ok icon */ |
|
137 |
margin-right: 0.5rem; |
|
138 |
font-family: FontAwesome; |
|
139 |
font-size: 100%; |
|
140 |
color: green; |
|
141 |
} |
src/authentic2/static/authentic2/css/style.css | ||
---|---|---|
76 | 76 |
.a2-log-message { |
77 | 77 |
white-space: pre-wrap; |
78 | 78 |
} |
79 | ||
80 |
.a2-registration-completion { |
|
81 |
padding: 1rem; |
|
82 |
width: 100%; |
|
83 |
min-width: 330px; |
|
84 |
} |
|
85 | ||
86 |
.a2-registration-completion input, |
|
87 |
.a2-registration-completion select, |
|
88 |
.a2-registration-completion textarea |
|
89 |
{ |
|
90 |
width: 100%; |
|
91 |
} |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
1 |
"use strict"; |
|
2 |
/* globals $, window, console, document */ |
|
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 |
/* |
|
23 |
* toggle error/ok on element with class names same as the validation code names |
|
24 |
* (cf. error_codes in authentic2.validators.validate_password) |
|
25 |
*/ |
|
26 |
var validatePassword = function(event) { |
|
27 |
var $this = $(event.target); |
|
28 |
if (!$this.val()) return; |
|
29 |
var policyContainer = $('#a2-password-policy-helper-' + $this.attr('name')); |
|
30 |
$.ajax({ |
|
31 |
method: 'POST', |
|
32 |
url: '/api/validate-password/', |
|
33 |
data: JSON.stringify({'password': $this.val()}), |
|
34 |
dataType: 'json', |
|
35 |
contentType: 'application/json; charset=utf-8', |
|
36 |
success: function(data) { |
|
37 |
if (data.result) { |
|
38 |
policyContainer |
|
39 |
.children('span') |
|
40 |
.removeClass('password-error password-ok'); |
|
41 |
data.validation.forEach(function (error) { |
|
42 |
// error is like [True, 'validation-code-name'] |
|
43 |
if (!error[0]) { |
|
44 |
toggleError(policyContainer.find('.' + error[1])); |
|
45 |
} else { |
|
46 |
toggleOk(policyContainer.find('.' + error[1])); |
|
47 |
} |
|
48 |
}); |
|
49 |
} |
|
50 |
} |
|
51 |
}); |
|
52 |
} |
|
53 |
/* |
|
54 |
* Check password equality |
|
55 |
*/ |
|
56 |
var displayPasswordEquality = function($input, $inputTarget) { |
|
57 |
var messages = $('#a2-password-equality-helper-' + $input.attr('name')); |
|
58 |
var form = $input.parents('form'); |
|
59 |
if ($inputTarget === undefined) { |
|
60 |
$inputTarget = form.find('input[type=password]:not(input[name='+$input.attr('name')+'])'); |
|
61 |
} |
|
62 |
if (!$input.val() || !$inputTarget.val()) return; |
|
63 |
if ($inputTarget.val() !== $input.val()) { |
|
64 |
toggleError(messages); |
|
65 |
} else { |
|
66 |
toggleOk(messages); |
|
67 |
} |
|
68 |
} |
|
69 |
var passwordEquality = function () { |
|
70 |
var $this = $(this); |
|
71 |
displayPasswordEquality($this); |
|
72 |
} |
|
73 |
/* |
|
74 |
* Hide and show password handlers |
|
75 |
*/ |
|
76 |
var showPassword = function (event) { |
|
77 |
var $this = $(event.target); |
|
78 |
$this.addClass('hide-password-button'); |
|
79 |
var name = $this.attr('id').split('a2-password-show-button-')[1]; |
|
80 |
$('[name='+name+']').attr('type', 'text'); |
|
81 |
event.preventDefault(); |
|
82 |
} |
|
83 |
var hidePassword = function (event) { |
|
84 |
var $this = $(event.target); |
|
85 |
window.setTimeout(function () { |
|
86 |
$this.removeClass('hide-password-button'); |
|
87 |
var name = $this.attr('id').split('a2-password-show-button-')[1]; |
|
88 |
$('[name='+name+']').attr('type', 'password'); |
|
89 |
}, 3000); |
|
90 |
} |
|
91 |
/* |
|
92 |
* Show the last character |
|
93 |
*/ |
|
94 |
var showLastChar = function(event) { |
|
95 |
if (event.keyCode == 32 || event.key === undefined || event.key == "" |
|
96 |
|| event.key == "Unidentified" || event.key.length > 1) { |
|
97 |
return; |
|
98 |
} |
|
99 |
var duration = 1000; |
|
100 |
$('#a2-password-show-last-'+$(event.target).attr('name')) |
|
101 |
.text(event.key) |
|
102 |
.animate({'opacity': 1}, { |
|
103 |
duration: 50, |
|
104 |
queue: false, |
|
105 |
complete: function () { |
|
106 |
var $this = $(this); |
|
107 |
window.setTimeout( |
|
108 |
debounce(function () { |
|
109 |
$this.animate({'opacity': 0}, { |
|
110 |
duration: 50 |
|
111 |
}); |
|
112 |
}, duration), duration); |
|
113 |
} |
|
114 |
}); |
|
115 |
} |
|
116 |
/* |
|
117 |
* Init events |
|
118 |
*/ |
|
119 |
/* add password validation and equality check event handlers */ |
|
120 |
$('form input[type=password]:not(input[data-check-policy])').each(function () { |
|
121 |
$('#a2-password-policy-helper-' + $(this).attr('name')).hide(); |
|
122 |
}); |
|
123 |
$('body').on('keyup', 'form input[data-check-policy]', validatePassword); |
|
124 |
$('body').on('keyup', 'form input[data-check-equality]', passwordEquality); |
|
125 |
/* |
|
126 |
* Add event to handle displaying error/OK |
|
127 |
* while editing the first password |
|
128 |
* only if the second one is not empty |
|
129 |
*/ |
|
130 |
$('input[data-check-equality]') |
|
131 |
.each(function () { |
|
132 |
var $input2 = $(this); |
|
133 |
$('body') |
|
134 |
.on('keyup', 'form input[type=password]:not([name=' + $input2.attr('name') + '])', |
|
135 |
function (event) { |
|
136 |
var $input1 = $(event.target); |
|
137 |
if ($input2.val().length) { |
|
138 |
displayPasswordEquality($input2, $input1); |
|
139 |
} |
|
140 |
}); |
|
141 |
}); |
|
142 |
/* add the a2-password-show-button after the first input */ |
|
143 |
$('input[data-show-all]') |
|
144 |
.each(function () { |
|
145 |
var $this = $(this); |
|
146 |
if (!$('#a2-password-show-button-' + $this.attr('name')).length) { |
|
147 |
$(this).after($('<i class="a2-password-show-button" id="a2-password-show-button-' |
|
148 |
+ $this.attr('name') + '"></i>') |
|
149 |
.on('mousedown', showPassword) |
|
150 |
.on('mouseup mouseleave', hidePassword) |
|
151 |
); |
|
152 |
} |
|
153 |
}); |
|
154 |
/* show the last character on keypress */ |
|
155 |
$('input[data-show-last]') |
|
156 |
.each(function () { |
|
157 |
var $this = $(this); |
|
158 |
if (!$('#a2-password-show-last-' + $this.attr('name')).length) { |
|
159 |
var offset = $this.offset(); |
|
160 |
offset.top = "calc(" + offset.top + "px + 0.2ex)"; |
|
161 |
offset.left = "calc(" + offset.left + "px + " + $this.width() + "px + 1.2ex)"; |
|
162 |
// on crée un div placé dans le padding-right de l'input |
|
163 |
var $span = $('<span class="a2-password-show-last" id="a2-password-show-last-' |
|
164 |
+ $this.attr('name') + '"></span>)') |
|
165 |
$span.css({ |
|
166 |
'font-size': $this.css('font-size'), |
|
167 |
'font-family': $this.css('font-family'), |
|
168 |
'line-height': $this.css('line-height'), |
|
169 |
'vertical-align': $this.css('vertical-align'), |
|
170 |
'padding-top': $this.css('padding-top'), |
|
171 |
'padding-bottom': $this.css('padding-bottom') |
|
172 |
}); |
|
173 |
$this.after($span); |
|
174 |
} |
|
175 |
}); |
|
176 |
$('body').on('keyup', 'form input[data-show-last]', showLastChar); |
|
177 |
}); |
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 |
{% if features.check_policy %} |
|
4 |
<p>{% trans "In order to create a secure password, please comply at least these rules :" %}</p> |
|
5 |
<ul class="a2-password-policy-helper" id="a2-password-policy-helper-{{ widget.attrs.name }}"> |
|
6 |
{% comment %}Class names are the same as the validation code names (cf. error_codes in authentic2.validators.validate_password){% endcomment %} |
|
7 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %} |
|
8 |
<li class="a2-min-length-policy">{% blocktrans with A2_PASSWORD_POLICY_MIN_LENGTH=app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters.{% endblocktrans %}</li> |
|
9 |
{% endif %} |
|
10 |
{% if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES %} |
|
11 |
<li class="a2-min-class-policy">{% blocktrans with A2_PASSWORD_POLICY_MIN_CLASSES=app_settings.A2_PASSWORD_POLICY_MIN_CLASSES %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_CLASSES }} classes among: lowercase letters, uppercase letters, digits and punctuations.{% endblocktrans %}</li> |
|
12 |
{% endif %} |
|
13 |
{% if app_settings.A2_PASSWORD_POLICY_REGEX %} |
|
14 |
{% if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %} |
|
15 |
<li class="a2-regexp-policy">{% blocktrans with A2_PASSWORD_POLICY_REGEX_ERROR_MSG=app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</li> |
|
16 |
{% else %} |
|
17 |
<li class="a2-regexp-policy">{% blocktrans with A2_PASSWORD_POLICY_REGEX=app_settings.A2_PASSWORD_POLICY_REGEX %}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 %}</li> |
|
18 |
{% endif %} |
|
19 |
{% endif %} |
|
20 |
</ul> |
|
21 |
{% endif %} |
|
22 |
{% if features.check_equality %} |
|
23 |
<ul class="a2-passwords-messages" id="a2-password-equality-helper-{{ widget.attrs.name }}"> |
|
24 |
<li class="a2-passwords-matched">{% trans 'Passwords match.' %}</li> |
|
25 |
<li class="a2-passwords-unmatched">{% trans 'Passwords do not match.' %}</li> |
|
26 |
</ul> |
|
27 |
{% 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/registration/registration_completion_form.html | ||
---|---|---|
25 | 25 |
{% block content %} |
26 | 26 |
<h2>{% trans "Registration" %}</h2> |
27 | 27 |
<p>{% trans "Please fill the form to complete your registration" %}</p> |
28 |
<form method="post"> |
|
28 |
<form method="post" class="a2-registration-completion">
|
|
29 | 29 |
{% csrf_token %} |
30 | 30 |
{{ form.as_p }} |
31 | 31 |
<button class="submit-button">{% trans 'Submit' %}</button> |
src/authentic2/validators.py | ||
---|---|---|
80 | 80 | |
81 | 81 |
email_validator = EmailValidator() |
82 | 82 | |
83 |
def validate_password(password): |
|
83 | ||
84 |
def password_validation_rules(password): |
|
85 |
""" |
|
86 |
Indicate whether or not the password complies each password validation rule |
|
87 |
return a list of tuples like (True or False, 'policy-code-name') |
|
88 |
""" |
|
84 | 89 |
password_set = set(password) |
85 | 90 |
digits = set(string.digits) |
86 | 91 |
lower = set(string.lowercase) |
... | ... | |
90 | 95 | |
91 | 96 |
if not password: |
92 | 97 |
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))
|
|
98 |
if len(password) < app_settings.A2_PASSWORD_POLICY_MIN_LENGTH:
|
|
99 |
errors.append((False, 'a2-min-length-policy'))
|
|
100 |
else:
|
|
101 |
errors.append((True, 'a2-min-length-policy'))
|
|
97 | 102 | |
98 | 103 |
class_count = 0 |
99 | 104 |
for cls in (digits, lower, upper, punc): |
100 | 105 |
if not password_set.isdisjoint(cls): |
101 | 106 |
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)) |
|
107 |
if class_count < app_settings.A2_PASSWORD_POLICY_MIN_CLASSES:
|
|
108 |
errors.append((False, 'a2-min-class-policy'))
|
|
109 |
else:
|
|
110 |
errors.append((True, 'a2-min-class-policy'))
|
|
111 | ||
107 | 112 |
if app_settings.A2_PASSWORD_POLICY_REGEX: |
108 | 113 |
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) |
|
114 |
errors.append((False, 'a2-regexp-policy')) |
|
115 |
else: |
|
116 |
errors.append((True, 'a2-regexp-policy')) |
|
117 | ||
118 |
return errors |
|
119 | ||
120 |
def validate_password(password): |
|
121 |
error_codes = { |
|
122 |
'a2-min-length-policy': _('password must contain at least %d characters') % app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
123 |
'a2-min-class-policy': _('password must contain characters ' |
|
124 |
'from at least %d classes among: lowercase letters, ' |
|
125 |
'uppercase letters, digits, and punctuations') % app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
126 |
'a2-regexp-policy': app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG or |
|
127 |
_('your password dit not match the regular expression %s') % app_settings.A2_PASSWORD_POLICY_REGEX |
|
128 |
} |
|
129 |
validation_exceptions = [] |
|
130 |
for (is_success, validation_code) in password_validation_rules(password): |
|
131 |
if not is_success: |
|
132 |
validation_exceptions.append(ValidationError( |
|
133 |
error_codes[validation_code])) |
|
134 | ||
135 |
if validation_exceptions: |
|
136 |
raise ValidationError(validation_exceptions) |
|
114 | 137 | |
115 | 138 | |
116 | 139 |
class UsernameValidator(RegexValidator): |
... | ... | |
135 | 158 |
'classes among: lowercase letters, uppercase letters, digits ' |
136 | 159 |
'and punctuations.') % {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} |
137 | 160 |
if app_settings.A2_PASSWORD_POLICY_REGEX: |
138 |
yield ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) or \ |
|
139 |
ugettext('Your password must match the regular expression: ' |
|
140 |
'%(regexp)s, please change this message using the ' |
|
141 |
'A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ |
|
142 |
{'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} |
|
161 |
if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG: |
|
162 |
yield ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) |
|
163 |
else: |
|
164 |
yield ugettext('Your password must match the regular expression: ' |
|
165 |
'%(regexp)s, please change this message using the ' |
|
166 |
'A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ |
|
167 |
{'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} |
|
168 | ||
143 | 169 | |
144 | 170 |
def password_help_text(): |
145 | 171 |
return ' '.join(__password_help_text_helper()) |
146 | 172 | |
173 | ||
147 | 174 |
password_help_text = lazy(password_help_text, six.text_type) |
tests/test_api.py | ||
---|---|---|
826 | 826 |
app.authorization = ('Basic', (user.username, user.username)) |
827 | 827 |
resp = app.get('/api/users/') |
828 | 828 |
assert 'A2_OPENED_SESSION' not in app.cookies |
829 | ||
830 | ||
831 |
def test_api_validate_password_bad_request(app): |
|
832 |
payload = { |
|
833 |
'bad_arg': 'boby lapointe' |
|
834 |
} |
|
835 |
resp = app.post_json(reverse('a2-api-validate-password'), params=payload, status=400) |
|
836 |
assert resp.json['result'] == 0 |
|
837 |
assert set(['password']) == set(resp.json['errors']) |
|
838 | ||
839 | ||
840 |
def test_api_validate_password_bad_password(app, settings): |
|
841 |
settings.A2_PASSWORD_POLICY_REGEX = '^[0-9]$' |
|
842 |
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 100 |
|
843 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 100 |
|
844 |
payload = { |
|
845 |
'password': 'boby2lapointe' |
|
846 |
} |
|
847 |
resp = app.post_json(reverse('a2-api-validate-password'), params=payload, status=200) |
|
848 |
assert resp.json['result'] == 1 |
|
849 |
assert [False, 'a2-min-length-policy'] in resp.json['validation'] |
|
850 |
assert [False, 'a2-min-class-policy'] in resp.json['validation'] |
|
851 |
assert [False, 'a2-regexp-policy'] in resp.json['validation'] |
|
852 | ||
853 | ||
854 |
def test_api_validate_password_good_password(app, settings): |
|
855 |
settings.A2_PASSWORD_POLICY_REGEX = '^[a-z]*$' |
|
856 |
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 4 |
|
857 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 0 |
|
858 |
payload = { |
|
859 |
'password': 'abcd' |
|
860 |
} |
|
861 |
resp = app.post_json(reverse('a2-api-validate-password'), params=payload, status=200) |
|
862 |
assert resp.json['result'] == 1 |
|
863 |
assert [True, 'a2-min-length-policy'] in resp.json['validation'] |
|
864 |
assert [True, 'a2-min-class-policy'] in resp.json['validation'] |
|
865 |
assert [True, 'a2-regexp-policy'] in resp.json['validation'] |
tests/test_registration.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 | |
3 |
import re |
|
3 | 4 |
from urlparse import urlparse |
4 | 5 | |
5 | 6 |
from django.core.urlresolvers import reverse |
... | ... | |
585 | 586 |
response = response.form.submit() |
586 | 587 |
assert new_next_url in response.content |
587 | 588 | |
589 | ||
590 |
def test_registration_activate_passwords_not_equal(app, db, settings, mailoutbox): |
|
591 |
settings.LANGUAGE_CODE = 'en-us' |
|
592 |
settings.A2_VALIDATE_EMAIL_DOMAIN = can_resolve_dns() |
|
593 |
settings.A2_EMAIL_IS_UNIQUE = True |
|
594 | ||
595 |
response = app.get(reverse('registration_register')) |
|
596 |
response.form.set('email', 'testbot@entrouvert.com') |
|
597 |
response = response.form.submit() |
|
598 |
response = response.follow() |
|
599 |
link = get_link_from_mail(mailoutbox[0]) |
|
600 |
response = app.get(link) |
|
601 |
response.form.set('password1', 'azerty12AZ') |
|
602 |
response.form.set('password2', 'AAAazerty12AZ') |
|
603 |
response = response.form.submit() |
|
604 |
assert "The two password fields didn't match." in response.content |
|
605 | ||
606 | ||
607 |
def test_registration_activate_assisted_password(app, db, settings, mailoutbox): |
|
608 |
settings.LANGUAGE_CODE = 'en-us' |
|
609 |
settings.A2_VALIDATE_EMAIL_DOMAIN = can_resolve_dns() |
|
610 |
settings.A2_EMAIL_IS_UNIQUE = True |
|
611 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = 3 |
|
612 |
settings.A2_PASSWORD_POLICY_MIN_LENGTH = 10 |
|
613 |
settings.A2_PASSWORD_POLICY_REGEX = '^.*$' |
|
614 |
response = app.get(reverse('registration_register')) |
|
615 |
response.form.set('email', 'testbot@entrouvert.com') |
|
616 |
response = response.form.submit() |
|
617 |
response = response.follow() |
|
618 |
link = get_link_from_mail(mailoutbox[0]) |
|
619 |
response = app.get(link) |
|
620 |
assert "password.js" in response.content |
|
621 |
assert "password.css" in response.content |
|
622 |
assert re.search('<input class="a2-password-assisted".*data-check-equality.*>', response.content, re.I | re.M | re.S) |
|
623 |
assert re.search('<input class="a2-password-assisted".*data-check-policy.*>', response.content, re.I | re.M | re.S) |
|
624 |
assert re.search('<input class="a2-password-assisted".*data-show-last.*>', response.content, re.I | re.M | re.S) |
|
625 |
assert re.search('class="a2-passwords-messages" id="a2-password-equality-helper-', response.content, re.I | re.M | re.S) |
|
626 |
assert re.search('class="a2-password-policy-helper" id="a2-password-policy-helper-', response.content, re.I | re.M | re.S) |
|
627 |
assert re.search('class="a2-min-length-policy"', response.content, re.I | re.M | re.S) |
|
628 |
assert re.search('class="a2-min-class-policy"', response.content, re.I | re.M | re.S) |
|
629 |
assert re.search('class="a2-regexp-policy"', response.content, re.I | re.M | re.S) |
|
630 | ||
631 | ||
632 |
def test_registration_activate_password_no_min_class_count(app, db, settings, mailoutbox): |
|
633 |
settings.A2_PASSWORD_POLICY_MIN_CLASSES = False |
|
634 |
response = app.get(reverse('registration_register')) |
|
635 |
response.form.set('email', 'testbot@entrouvert.com') |
|
636 |
response = response.form.submit() |
|
637 |
response = response.follow() |
|
638 |
link = get_link_from_mail(mailoutbox[0]) |
|
639 |
response = app.get(link) |
|
640 |
assert not re.search('class="a2-min-class-policy"', response.content, re.I | re.M | re.S) |
|
588 |
- |