Projet

Général

Profil

0001-create-AssistedPassword-AssistedPasswordFormMixin-an.patch

Anonyme, 21 juin 2018 19:25

Télécharger (17,8 ko)

Voir les différences:

Subject: [PATCH] create AssistedPassword, AssistedPasswordFormMixin and a
 validation UI (#24438)

 src/authentic2/app_settings.py                |   3 +
 src/authentic2/registration_backend/forms.py  |  68 ++++++++-
 .../static/authentic2/css/password.css        |  73 +++++++++
 .../static/authentic2/js/password.js          | 139 ++++++++++++++++++
 .../authentic2/widgets/assisted_password.html |   8 +
 .../templates/authentic2/widgets/attrs.html   |   2 +
 src/authentic2/validators.py                  |  40 +++--
 7 files changed, 311 insertions(+), 22 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/authentic2/widgets/assisted_password.html
 create mode 100644 src/authentic2/templates/authentic2/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
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
    password_policy = {
37
        'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH,
38
        'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES,
39
        'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX,
40
    }
41
    messages_class = 'a2-passwords-messages'
42
    template_name = 'authentic2/widgets/assisted_password.html'
43

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

  
66

  
67
class AssistedPasswordFormMixin(Form):
68
    class Media:
69
        js = ('authentic2/js/password.js',)
70
        css = {'all': ('authentic2/css/password.css',)}
71

  
72
    password1 = CharField(
73
        widget=AssistedPasswordInput(attrs={
74
            'data-show-last': app_settings.A2_PASSWORD_DISPLAY_LAST_CHAR_DURATION,
75
            'data-show-all': app_settings.A2_PASSWORD_DISPLAY_SHOW_ALL,
76
            'data-check-policy': app_settings.A2_PASSWORD_DISPLAY_CHECK_POLICY,
77
        }),
78
        label=_("Password"),
79
        validators=[validators.validate_password],
80
        help_text=validators.password_help_text())
81

  
82
    password2 = CharField(
83
        widget=AssistedPasswordInput(attrs={
84
            'data-check-equality-against': 'password1',
85
        }),
86
        label=_("Password (again)"))
87

  
27 88

  
28 89
class RegistrationForm(Form):
29 90
    error_css_class = 'form-field-error'
......
114 175
        return user
115 176

  
116 177

  
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

  
178
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword, AssistedPasswordFormMixin):
123 179
    def clean(self):
124 180
        """
125 181
        Verifiy that the values entered into the two password fields
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
.a2-show-password-button {
27
  padding-left: 0.5em;
28
  cursor: pointer;
29
}
30

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

  
37
.hide-password-button:after {
38
  content: "\f070"; /* crossed eye */
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
	/*
78
	* Show the last character
79
	*/
80
	var showLastChar = function(event) {
81
		if (event.keyCode == 32 || event.key === undefined || event.key == "" || event.key == "Unidentified" || event.key.length > 1) {
82
			return;
83
		}
84
		var duration = Number.isNaN(parseInt($(this).data('showLast'))) ? 300 : parseInt($(this).data('showLast'));
85
		$('#a2-show-last-'+$(this).attr('id'))
86
			.html(event.key)
87
			.fadeIn('fast', function () {
88
				var $this = $(this);
89
					window.setTimeout(function() {
90
						$this.fadeOut('fast', function () {
91
							$this.html('&nbsp;');
92
							$this.css('display', 'inline');
93
						});
94
					}, duration);
95
			});
96
	}
97
	/* add password validation and equality check event handlers */
98
	$('form').on('keyup', 'input[data-check-policy]', validatePassword);
99
	$('form').on('keyup', 'input[data-check-equality-against]', passwordEquality);
100
	/* while editing the first password, toggleError if the second one is not empty */
101
	$('input[data-check-equality-against]')
102
		.each(function () {
103
			var input2 = $(this);
104
			var input1 = $('form').find('input[name='+input2.data('checkEqualityAgainst')+']');
105
			$('form').on('keyup', input1, function () {
106
				var form = $(this)
107
				var messages = form.find('.'+input2.data('passwordMessagesClass'));
108
				if (input2.val().length) {
109
					if (input1.val() !== input2.val()) {
110
						toggleError(messages);
111
					} else {
112
						toggleOk(messages);
113
					}
114
				}
115
			});
116
		});
117

  
118
	/* add the a2-show-password-button after the first input */
119
	$('input[data-show-all]')
120
		.each(function () {
121
			var $this = $(this);
122
			if (!$('#a2-show-password-button-'+$this.attr('id')).length) {
123
				$(this).after($('<i class="a2-show-password-button" id="a2-show-password-button-'+$this.attr('id')+'"></i>')
124
					.on('mousedown', showPassword)
125
					.on('mouseup', hidePassword)
126
				);
127
			}
128
		});
129
	/* show the last character on keypress */
130
	$('input[data-show-last]')
131
		.each(function () {
132
			var $this = $(this);
133
			if (!$('#a2-show-last-'+$this.attr('id')).length) {
134
				$this
135
					.after($('<div class="a2-password-show-last" id="a2-show-last-'+$this.attr('id')+'">&nbsp;</div>'));
136
			}
137
		});
138
	$('form').on('keyup', 'input[data-show-last]', showLastChar);
139
});
src/authentic2/templates/authentic2/widgets/assisted_password.html
1
{% load i18n %}
2
<input {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "authentic2/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/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/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
-