Projet

Général

Profil

0001-create-AssistedPassword-AssistedPasswordFormMixin-an.patch

Anonyme, 22 juin 2018 12:23

Télécharger (20 ko)

Voir les différences:

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

 src/authentic2/app_settings.py                |   5 +
 src/authentic2/registration_backend/forms.py  |  68 +++++++-
 .../static/authentic2/css/password.css        |  73 +++++++++
 .../static/authentic2/js/password.js          | 150 ++++++++++++++++++
 .../authentic2/password_help_text.html        |  19 +++
 .../authentic2/widgets/assisted_password.html |   8 +
 .../templates/authentic2/widgets/attrs.html   |   2 +
 src/authentic2/validators.py                  |  40 ++---
 8 files changed, 332 insertions(+), 33 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/password_help_text.html
 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=1000, 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'),
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
    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' if app_settings.A2_PASSWORD_DISPLAY_CHECK_EQUALITY else False,
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
  opacity: 0;
72
  padding: 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 debounce = function (func, milliseconds) {
81
		var timer;
82
		return function() {
83
			window.clearTimeout(timer);
84
			timer = window.setTimeout(function() {
85
				func();
86
			}, milliseconds);
87
		};
88
	}
89
	var showLastChar = function(event) {
90
		if (event.keyCode == 32 || event.key === undefined || event.key == "" || event.key == "Unidentified" || event.key.length > 1) {
91
			return;
92
		}
93
		var duration = Number.isNaN(parseInt($(this).data('showLast'))) ? 300 : parseInt($(this).data('showLast'));
94
		$('#a2-show-last-'+$(this).attr('id'))
95
			.html(event.key)
96
			.animate({'opacity': 1}, {
97
				duration: 50,
98
				queue: false,
99
				complete: function () {
100
					var $this = $(this);
101
					window.setTimeout(
102
						debounce(function () {
103
							$this.animate({'opacity': 0}, {duration: 50});
104
						}, duration), duration);
105
				}
106
			});
107
	}
108
	/* add password validation and equality check event handlers */
109
	$('form').on('keyup', 'input[data-check-policy]', validatePassword);
110
	$('form').on('keyup', 'input[data-check-equality-against]', passwordEquality);
111
	/* while editing the first password, toggleError if the second one is not empty */
112
	$('input[data-check-equality-against]')
113
		.each(function () {
114
			var input2 = $(this);
115
			var input1 = $('form').find('input[name='+input2.data('checkEqualityAgainst')+']');
116
			$('form').on('keyup', input1, function () {
117
				var form = $(this)
118
				var messages = form.find('.'+input2.data('passwordMessagesClass'));
119
				if (input2.val().length) {
120
					if (input1.val() !== input2.val()) {
121
						toggleError(messages);
122
					} else {
123
						toggleOk(messages);
124
					}
125
				}
126
			});
127
		});
128

  
129
	/* add the a2-show-password-button after the first input */
130
	$('input[data-show-all]')
131
		.each(function () {
132
			var $this = $(this);
133
			if (!$('#a2-show-password-button-'+$this.attr('id')).length) {
134
				$(this).after($('<i class="a2-show-password-button" id="a2-show-password-button-'+$this.attr('id')+'"></i>')
135
					.on('mousedown', showPassword)
136
					.on('mouseup', hidePassword)
137
				);
138
			}
139
		});
140
	/* show the last character on keypress */
141
	$('input[data-show-last]')
142
		.each(function () {
143
			var $this = $(this);
144
			if (!$('#a2-show-last-'+$this.attr('id')).length) {
145
				$this
146
					.after($('<div class="a2-password-show-last" id="a2-show-last-'+$this.attr('id')+'">&nbsp;</div>'));
147
			}
148
		});
149
	$('form').on('keyup', 'input[data-show-last]', showLastChar);
150
});
src/authentic2/templates/authentic2/password_help_text.html
1
{% load i18n %}
2
{% if A2_PASSWORD_POLICY_MIN_LENGTH and A2_PASSWORD_POLICY_MIN_CLASSES %}
3
	<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters{% endblocktrans %}</span>&nbsp;{% trans 'from' %}&nbsp;<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>
4
{% else %}
5
	{% if A2_PASSWORD_POLICY_MIN_LENGTH %}
6
	<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters.{% endblocktrans %}</span>
7
	{% endif %}
8
	{% if A2_PASSWORD_POLICY_MIN_CLASSES %}
9
	<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>
10
	{% endif %}
11
{% endif %}
12
{% if A2_PASSWORD_POLICY_REGEX %}
13
	{% if A2_PASSWORD_POLICY_REGEX_ERROR_MSG %}
14
		<span class="a2-regexp-policy">{% blocktrans %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</span>
15
	{% else %}
16
		<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>
17
	{% endif %}
18

  
19
{% endif %}
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
5 5

  
6 6
import smtplib
7 7

  
8
from django.utils.translation import ugettext_lazy as _, ugettext
8
from django.utils.translation import ugettext_lazy as _
9 9
from django.utils.encoding import force_text
10 10
from django.core.exceptions import ValidationError
11 11
from django.core.validators import RegexValidator
12
from django.utils.functional import lazy
12
from django.template.loader import render_to_string
13
from django.utils.safestring import mark_safe
13 14

  
14 15
import socket
15 16
import dns.resolver
......
119 120
        super(UsernameValidator, self).__init__(*args, **kwargs)
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}
130
    else:
131
        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}
133
        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}
137
    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}
143

  
144 123
def password_help_text():
145
    return ' '.join(__password_help_text_helper())
146

  
147
password_help_text = lazy(password_help_text, six.text_type)
124
    '''
125
    Password fields help_text
126
    '''
127
    context = {
128
        'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH,
129
        'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES,
130
        'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX,
131
        'A2_PASSWORD_POLICY_REGEX_ERROR_MSG': app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG
132
    }
133
    return mark_safe(render_to_string(app_settings.A2_PASSWORD_POLICY_MESSAGE_TPL, context))
148
-