Projet

Général

Profil

0001-add-password-validation-interfacein-registration-for.patch

Anonyme, 21 juin 2018 16:04

Télécharger (16,8 ko)

Voir les différences:

Subject: [PATCH] add password validation interfacein registration form
 (#24438)

 src/authentic2/app_settings.py                |   3 +
 src/authentic2/registration_backend/forms.py  |  57 +++++++-
 .../static/authentic2/css/password.css        |  73 ++++++++++
 .../static/authentic2/js/password.js          | 128 ++++++++++++++++++
 .../widgets/assisted_password.html            |   8 ++
 .../templates/registration/widgets/attrs.html |   2 +
 src/authentic2/validators.py                  |  40 +++---
 7 files changed, 291 insertions(+), 20 deletions(-)
 create mode 100644 src/authentic2/static/authentic2/css/password.css
 create mode 100644 src/authentic2/static/authentic2/js/password.js
 create mode 100644 src/authentic2/templates/registration/widgets/assisted_password.html
 create mode 100644 src/authentic2/templates/registration/widgets/attrs.html
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_DURATION=Setting(default=300, definition='Minimum duration in milliseconds to display 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'),
143 146
    A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'),
144 147
    A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'),
145 148
    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
10
from django.forms.utils import flatatt
9 11
from django.utils.datastructures import SortedDict
12
from django.utils.encoding import force_text
10 13
from django.db.models.fields import FieldDoesNotExist
11 14
from django.forms.util import ErrorList
12 15

  
13 16
from django.contrib.auth.models import BaseUserManager, Group
14 17
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME
18
from django.utils.safestring import mark_safe
15 19
from django.core.mail import send_mail
16 20
from django.core import signing
17 21
from django.template import RequestContext
......
24 28

  
25 29
User = compat.get_user_model()
26 30

  
31
class AssistedPasswordInput(PasswordInput):
32
    """
33
    Custom Password Input with extra functionnality
34
    """
35
    password_policy = {
36
        'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH,
37
        'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES,
38
        'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX,
39
    }
40
    messages_class = 'a2-passwords-messages'
41
    template_name = 'registration/widgets/assisted_password.html'
42

  
43
    def render(self, name, value, attrs=None):
44
        if self.attrs.get('data-check-equality-against'):
45
            attrs['checkEquality'] = True
46
        # Remove this part down when dropping Django 1.8 to 1.10 compatibility
47
        if value is None:
48
            value = ''
49
        context = {
50
            'password_policy': json.dumps(self.password_policy), # to configure password.js using app_settings
51
            'messages_class': self.messages_class, # to configure the block for interactive messages
52
            'template_name': self.template_name, # to be used in templates to know who they are
53
            'widget': {}
54
        }
55
        context['widget']['attrs'] = self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type)
56
        if value != '':
57
            # Only add the 'value' attribute if a value is non-empty.
58
            context['widget']['value'] = force_text(self._format_value(value))
59
        return mark_safe(render_to_string(self.template_name, context))
60

  
27 61

  
28 62
class RegistrationForm(Form):
29 63
    error_css_class = 'form-field-error'
......
115 149

  
116 150

  
117 151
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)"))
152
    class Media:
153
        js = ('authentic2/js/password.js',)
154
        css = {'all': ('authentic2/css/password.css',)}
155

  
156
    password1 = CharField(
157
        widget=AssistedPasswordInput(attrs={
158
            'data-show-last': app_settings.A2_PASSWORD_DISPLAY_LAST_CHAR_DURATION,
159
            'data-show-all': app_settings.A2_PASSWORD_DISPLAY_SHOW_ALL,
160
            'data-check-policy': app_settings.A2_PASSWORD_DISPLAY_CHECK_POLICY,
161
        }),
162
        label=_("Password"),
163
        validators=[validators.validate_password],
164
        help_text=validators.password_help_text())
165

  
166
    password2 = CharField(
167
        widget=AssistedPasswordInput(attrs={
168
            'data-check-equality-against': 'password1',
169
        }),
170
        label=_("Password (again)"))
122 171

  
123 172
    def clean(self):
124 173
        """
src/authentic2/static/authentic2/css/password.css
1

  
2
.a2-min-class-policy, .a2-min-length-policy, .a2-regexp-policy {
3
  display: inline;
4
}
5

  
6
.password-error {
7
  font-weight: bold;
8
  color: red;
9
}
10

  
11
.password-error:before {
12
  content: "\f071";
13
  margin-right: 0.3em;
14
  font-family: FontAwesome;
15
  font-size: 100%;
16
  color: red;
17
}
18

  
19
.password-ok:before {
20
  content: "\f00c";
21
  font-family: FontAwesome;
22
  font-size: 100%;
23
  color: green;
24
}
25

  
26
.show-password-button {
27
  padding-left: 0.5em;
28
  cursor: pointer;
29
}
30

  
31
.show-password-button:after {
32
	content: "\f06e";
33
	font-family: FontAwesome;
34
	font-size: 150%;
35
}
36

  
37
.hide-password-button:after {
38
	content: "\f070";
39
	font-family: FontAwesome;
40
	font-size: 150%;
41
}
42

  
43
.a2-passwords-messages {
44
  display: none;
45
}
46

  
47
.a2-passwords-unmatched {
48
  display: none;
49
  color: red;
50
}
51

  
52
.a2-passwords-matched {
53
  display: none;
54
  color: green;
55
}
56

  
57
.password-error.a2-passwords-messages, .password-ok.a2-passwords-messages {
58
  display: block;
59
}
60

  
61
.password-error .a2-passwords-unmatched {
62
  display: inline;
63
}
64

  
65
.password-ok .a2-passwords-matched {
66
  display: inline;
67
}
68

  
69
.a2-password-show-last {
70
  display: inline;
71
  padding: 1em;
72
  min-width: 1em;
73
}
src/authentic2/static/authentic2/js/password.js
1
"use strict";
2
/* globals $, window, console */
3

  
4
$(function () {
5
	var toggleError = function($elt) {
6
		$elt.removeClass('password-ok');
7
		$elt.addClass('password-error');
8
	}
9
	var toggleOk = function($elt) {
10
		$elt.removeClass('password-error');
11
		$elt.addClass('password-ok');
12
	}
13
	var validatePassword = function () {
14
		var minClassElt = $(this).parents('form').find('.a2-min-class-policy');
15
		var minLengthElt = $(this).parents('form').find('.a2-min-length-policy');
16
		var regexpElt = $(this).parents('form').find('.a2-regexp-policy');
17
		$(this)
18
			.each(function () {
19
				var $this = $(this);
20
				var settings = $this.data('passwordPolicy');
21
				var password = $this.val();
22
				var min_len = settings.A2_PASSWORD_POLICY_MIN_LENGTH;
23
				if (min_len && password.length < min_len) {
24
					toggleError(minLengthElt);
25
				} else {
26
					toggleOk(minLengthElt);
27
				}
28
				var digits = /\d/g;
29
				var lowerCase = /[a-z]/g;
30
				var upperCase = /[A-Z]/g;
31
				var punctuation = /'!"#\$%&\\'\(\)\*\+,-\.\/:;<=>\?@\[\]\^_`\{\|\}~'/g;
32
				var minClassCount = settings.A2_PASSWORD_POLICY_MIN_CLASSES;
33
				var classCount = 0;
34
				if (minClassCount) {
35
					[digits, lowerCase, upperCase, punctuation].forEach(function (cls) {
36
						if (cls.test(password)) classCount++;
37
					})
38
					if (classCount < minClassCount) {
39
						toggleError(minClassElt);
40
					} else {
41
						toggleOk(minClassElt);
42
					}
43
				}
44
				if (settings.A2_PASSWORD_POLICY_REGEX) {
45
					var regExpObject = new RegExp(settings.A2_PASSWORD_POLICY_REGEX, 'g');
46
					if (!regExpObject.test(password)) {
47
						toggleError(regexpElt);
48
					} else {
49
						toggleOk(regexpElt);
50
					}
51
				}
52
			});
53
	}
54
	var passwordEquality = function () {
55
		$(this)
56
			.each(function () {
57
				var input = $(this);
58
				var form = input.parents('form');
59
				var messages = form.find('.'+input.data('passwordMessagesClass'));
60
				var inputTarget = form.find('input[name='+input.data('checkEqualityAgainst')+']');
61
				if (!input.val() || !inputTarget.val()) return;
62
				if (inputTarget.val() !== input.val()) {
63
					toggleError(messages);
64
				} else {
65
					toggleOk(messages);
66
				}
67
			});
68
	}
69
	var showPassword = function () {
70
		$(this).addClass('hide-password-button');
71
		$(this).prevUntil().filter('input[data-show-all]').last().attr('type', 'text');
72
	}
73
	var hidePassword = function () {
74
		$(this).removeClass('hide-password-button');
75
		$(this).prevUntil().filter('input[data-show-all]').last().attr('type', 'password');
76
	}
77
	var showLastChar = function(event) {
78
		if (event.keyCode == 32 || event.key === undefined || event.key == "" || event.key == "Unidentified" || event.key.length > 1) {
79
			return;
80
		}
81

  
82
		var duration = Number.isNaN(parseInt($(this).data('showLast'))) ? 300 : parseInt($(this).data('showLast'));
83
		$(this).next('.a2-password-show-last')
84
			.html(event.key)
85
			.fadeIn(duration, function () {
86
					$(this).fadeOut(duration, function () {
87
						$(this).html('&nbsp;');
88
						$(this).css('display', 'inline');
89
					});
90
			});
91
	}
92
	/* add password validation and equality check event handlers */
93
	$('form').on('keyup', 'input[data-check-policy]', validatePassword);
94
	$('form').on('keyup', 'input[data-check-equality-against]', passwordEquality);
95
	/* while editing the first password, toggleError if the second one is not empty */
96
	$('input[data-check-equality-against]')
97
		.each(function () {
98
			var input2 = $(this);
99
			var input1 = $('form').find('input[name='+input2.data('checkEqualityAgainst')+']');
100
			$('form').on('keyup', input1, function () {
101
				var form = $(this)
102
				var messages = form.find('.'+input2.data('passwordMessagesClass'));
103
				if (input2.val().length) {
104
					if (input1.val() !== input2.val()) {
105
						toggleError(messages);
106
					} else {
107
						toggleOk(messages);
108
					}
109
				}
110
			});
111
		});
112

  
113
	/* add the show-password-button after the first input */
114
	$('input[data-show-all]')
115
		.each(function () {
116
			$(this).after($('<i class="show-password-button"></i>')
117
				.on('mousedown', showPassword)
118
				.on('mouseup', hidePassword)
119
			);
120
		});
121
	/* show the last character on keypress */
122
	$('input[data-show-last]')
123
		.each(function () {
124
			$(this)
125
				.after($('<div class="a2-password-show-last">&nbsp;</div>'));
126
		});
127
	$('form').on('keyup', 'input[data-show-last]', showLastChar);
128
});
src/authentic2/templates/registration/widgets/assisted_password.html
1
{% load i18n %}
2
<input {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "registration/widgets/attrs.html" %} data-password-policy='{% autoescape off %}{{ password_policy }}{% endautoescape %}' data-password-messages-class='{{ messages_class }}'>
3
{% if widget.attrs.checkEquality %}
4
<div class="{{ messages_class }}">
5
	<span class="a2-passwords-matched">{% trans 'Passwords match.' %}</span>
6
	<span class="a2-passwords-unmatched">{% trans 'Passwords do not match.' %}</span>
7
</div>
8
{% endif %}
src/authentic2/templates/registration/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/validators.py
120 120

  
121 121

  
122 122
def __password_help_text_helper():
123
    if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and \
124
            app_settings.A2_PASSWORD_POLICY_MIN_CLASSES:
125
        yield ugettext('Your password must contain at least %(min_length)d characters from at '
126
                'least %(min_classes)d classes among: lowercase letters, uppercase letters, '
127
                'digits and punctuations.') % {
128
                        'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH,
129
                        'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES}
123
    '''
124
    Password fields help_text
125
    '''
126
    min_length_html = '<span class="a2-min-length-policy">%s</span>' %\
127
        ugettext('Your password must contain at least %(min_length)d characters.' %
128
        {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH})
129

  
130
    min_class_html = '<span class="a2-min-class-policy">%s</span>' %\
131
        ugettext(('at least %(min_classes)d classes among: lowercase letters, uppercase letters, digits and punctuations.') %
132
        {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES})
133
    if app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG:
134
        regexp_html = '<span class="a2-regexp-policy">%s</span>' %\
135
            ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG)
136
    else:
137
        regexp_html = '<span class="a2-regexp-policy">%s</span>' %\
138
            ugettext('Your password must match the regular expression: %(regexp)s, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \
139
                {'regexp': app_settings.A2_PASSWORD_POLICY_REGEX}
140

  
141
    if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and app_settings.A2_PASSWORD_POLICY_MIN_CLASSES:
142
        yield '%s %s %s' % (min_length_html, _('from'), min_class_html)
130 143
    else:
131 144
        if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH:
132
            yield ugettext('Your password must contain at least %(min_length)d characters.') % {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH}
145
            yield min_length_html
133 146
        if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES:
134
            yield ugettext('Your password must contain characters from at least %(min_classes)d '
135
                    'classes among: lowercase letters, uppercase letters, digits '
136
                    'and punctuations.') % {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES}
147
            yield "%s %s" % (ugettext('Your password must contain characters from'), min_class_html)
137 148
    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}
149
        yield regexp_html
150

  
143 151

  
144 152
def password_help_text():
145 153
    return ' '.join(__password_help_text_helper())
146
-