Projet

Général

Profil

0001-create-assisted-password-input-widgets-24438.patch

Anonyme, 05 juillet 2018 16:43

Télécharger (31,8 ko)

Voir les différences:

Subject: [PATCH] create assisted password input widgets (#24438)

 src/authentic2/api_urls.py                    |   2 +
 src/authentic2/api_views.py                   |  34 ++++
 src/authentic2/registration_backend/forms.py  |  12 +-
 .../registration_backend/widgets.py           |  92 +++++++++
 .../static/authentic2/css/password.css        | 137 ++++++++++++++
 .../static/authentic2/css/style.css           |  19 ++
 .../static/authentic2/js/password.js          | 177 ++++++++++++++++++
 .../authentic2/widgets/assisted_password.html |  28 +++
 .../templates/authentic2/widgets/attrs.html   |   2 +
 .../registration_completion_form.html         |   2 +-
 src/authentic2/validators.py                  |  67 +++++--
 tests/test_api.py                             |  37 ++++
 tests/test_registration.py                    |  53 ++++++
 13 files changed, 636 insertions(+), 26 deletions(-)
 create mode 100644 src/authentic2/registration_backend/widgets.py
 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/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-icon {
8
	display: inline-block;
9
	width: calc(18em / 14);
10
	text-align: center;
11
	font-style: normal;
12
	padding-right: 1em;
13
}
14

  
15
/* default circle icon */
16
.a2-password-icon:before {
17
	font-family: FontAwesome;
18
  content: "\f111"; /* right hand icon */
19
  font-size: 50%;
20
}
21

  
22
.a2-password-policy-helper {
23
  display: flex;
24
  height: auto;
25
  flex-direction: row;
26
  flex-wrap: wrap;
27
  position: relative;
28
  padding: 0.5rem 1rem;
29
}
30

  
31
/* we don't want helptext when a2-password-policy-helper is here */
32
.a2-password-policy-helper ~ .helptext {
33
  display: none;
34
}
35

  
36
.a2-min-class-policy,
37
.a2-min-length-policy,
38
.a2-regexp-policy {
39
  flex: 1 1 0;
40
  list-style: none;
41
}
42

  
43
.password-error {
44
  color: black;
45
}
46

  
47
.password-ok {
48
  color: green;
49
}
50

  
51
.password-error .a2-password-icon:before {
52
  content: "\f00d"; /* cross icon */
53
  color: red;
54
}
55

  
56
.password-ok .a2-password-icon::before {
57
  content: "\f00c"; /* ok icon */
58
  color: green;
59
}
60

  
61
.a2-password-show-last {
62
  display: inline-block;
63
  opacity: 0;
64
  float: right;
65
  text-align: center;
66
  position: relative;
67
  right: 30px;
68
  top: -4.5ex;
69
  width: 20px;
70
}
71

  
72
.a2-password-show-button {
73
  display: inline-block;
74
  float: right;
75
  position: relative;
76
  padding: 0;
77
  width: 0px;
78
  right: 10px;
79
  top: -4ex;
80
  cursor: pointer;
81
}
82

  
83
.a2-password-show-button:after {
84
  content: "\f06e"; /* eye */
85
  font-family: FontAwesome;
86
  font-size: 125%;
87
}
88

  
89
.hide-password-button:after {
90
  content: "\f070"; /* crossed eye */
91
  font-family: FontAwesome;
92
  font-size: 125%;
93
}
94

  
95
.a2-passwords-messages {
96
  display: block;
97
  padding: 0.5rem 1rem;
98
}
99

  
100
.a2-passwords-default {
101
	list-style: none;
102
  opacity: 0;
103
}
104

  
105
.password-error .a2-passwords-default,
106
.password-ok .a2-passwords-default {
107
	display: none;
108
}
109

  
110
.a2-passwords-matched,
111
.a2-passwords-unmatched {
112
	display: none;
113
	list-style: none;
114
	opacity: 0;
115
	transition: all 0.3s ease;
116
}
117

  
118
.password-error.a2-passwords-messages:before,
119
.password-ok.a2-passwords-messages:before {
120
  display: none;
121
}
122

  
123
.password-error .a2-passwords-unmatched,
124
.password-ok .a2-passwords-matched {
125
	display: block;
126
	opacity: 1;
127
}
128

  
129
.password-error .a2-passwords-unmatched .a2-password-icon:before {
130
  content: "\f00d"; /* cross icon */
131
  color: red;
132
}
133

  
134
.password-ok .a2-passwords-matched .a2-password-icon:before {
135
  content: "\f00c"; /* ok icon */
136
  color: green;
137
}
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
  min-width: 330px;
83
  width: 50%;
84
}
85

  
86
@media screen and (max-width: 900px) {
87
  .a2-registration-completion {
88
    width: 100%;
89
  }
90
}
91

  
92
.a2-registration-completion input,
93
.a2-registration-completion select,
94
.a2-registration-completion textarea
95
{
96
  width: 100%;
97
}
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 use at least :" %}</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"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_MIN_LENGTH=app_settings.A2_PASSWORD_POLICY_MIN_LENGTH %}{{ 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"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_MIN_CLASSES=app_settings.A2_PASSWORD_POLICY_MIN_CLASSES %}{{ A2_PASSWORD_POLICY_MIN_CLASSES }} different kind of characters (between lowercase, uppercase, 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"><i class="a2-password-icon"></i>{% 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"><i class="a2-password-icon"></i>{% blocktrans with A2_PASSWORD_POLICY_REGEX=app_settings.A2_PASSWORD_POLICY_REGEX %}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-default"><i class="a2-password-icon"></i>{% trans 'Both passwords must match.' %}</li>
25
	<li class="a2-passwords-matched"><i class="a2-password-icon"></i>{% trans 'Passwords match.' %}</li>
26
	<li class="a2-passwords-unmatched"><i class="a2-password-icon"></i>{% trans 'Passwords do not match.' %}</li>
27
</ul>
28
{% 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&#39;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
-