Commit 91d859bb2708608fad75a8bab13dbe9952715e9a

Authored by Lucas Kanashiro
2 parents a43bc417 1aafcce7

Merge pull request #124 from colab/validate-passwd

Validate passwd
colab/accounts/forms.py
1 1 # -*- coding: utf-8 -*-
2 2  
  3 +from importlib import import_module
  4 +
3 5 from django import forms
4 6 from django.conf import settings
5 7 from django.contrib.auth import get_user_model
6   -from django.contrib.auth.forms import ReadOnlyPasswordHashField
  8 +from django.contrib.auth.forms import (ReadOnlyPasswordHashField,
  9 + SetPasswordForm, PasswordChangeForm)
7 10 from django.core.urlresolvers import reverse
8 11 from django.utils.functional import lazy
9 12 from django.utils.translation import ugettext_lazy as _
... ... @@ -160,7 +163,41 @@ class ListsForm(forms.Form):
160 163 choices=lazy(get_lists_choices, list)())
161 164  
162 165  
163   -class UserCreationForm(UserForm):
  166 +class ColabSetPasswordFormMixin(object):
  167 +
  168 + def apply_custom_validators(self, password):
  169 + for app in settings.COLAB_APPS.values():
  170 + if 'password_validators' in app:
  171 + for validator_path in app.get('password_validators'):
  172 + module_path, func_name = validator_path.rsplit('.', 1)
  173 + module = import_module(module_path)
  174 + validator_func = getattr(module, func_name, None)
  175 + if validator_func:
  176 + validator_func(password)
  177 +
  178 + return password
  179 +
  180 + def clean_new_password2(self):
  181 + try:
  182 + password = super(ColabSetPasswordFormMixin,
  183 + self).clean_new_password2()
  184 + except AttributeError:
  185 + password = self.cleaned_data['new_password2']
  186 +
  187 + self.apply_custom_validators(password)
  188 + return password
  189 +
  190 + def clean_password2(self):
  191 + try:
  192 + password = super(ColabSetPasswordFormMixin, self).clean_password2()
  193 + except AttributeError:
  194 + password = self.cleaned_data['password2']
  195 +
  196 + self.apply_custom_validators(password)
  197 + return password
  198 +
  199 +
  200 +class UserCreationForm(UserForm, ColabSetPasswordFormMixin):
164 201 """
165 202 A form that creates a user, with no privileges, from the given username and
166 203 password.
... ... @@ -231,6 +268,8 @@ class UserCreationForm(UserForm):
231 268 self.error_messages['password_mismatch'],
232 269 code='password_mismatch',
233 270 )
  271 +
  272 + super(UserCreationForm, self).clean_password2()
234 273 return password2
235 274  
236 275 def save(self, commit=True):
... ... @@ -282,3 +321,11 @@ class UserChangeForm(forms.ModelForm):
282 321 # This is done here, rather than on the field, because the
283 322 # field does not have access to the initial value
284 323 return self.initial["password"]
  324 +
  325 +
  326 +class ColabSetPasswordForm(ColabSetPasswordFormMixin, SetPasswordForm):
  327 + pass
  328 +
  329 +
  330 +class ColabPasswordChangeForm(ColabSetPasswordFormMixin, PasswordChangeForm):
  331 + pass
... ...
colab/accounts/models.py
... ... @@ -2,9 +2,9 @@
2 2  
3 3 import urlparse
4 4  
5   -from django.db import models
6 5 from django.contrib.auth.models import AbstractUser, UserManager
7 6 from django.core.urlresolvers import reverse
  7 +from django.db import models
8 8 from django.utils.crypto import get_random_string
9 9 from django.utils.translation import ugettext_lazy as _
10 10  
... ...
colab/accounts/tests/test_forms.py
... ... @@ -6,16 +6,50 @@ Objective: Test parameters, and behavior.
6 6 import datetime
7 7 from mock import patch
8 8  
9   -from django.test import TestCase
  9 +from django.test import TestCase, override_settings
10 10 from django.core.urlresolvers import reverse
11 11  
12   -from colab.accounts.forms import UserCreationForm, UserChangeForm,\
13   - UserUpdateForm, UserForm, get_lists_choices
  12 +from colab.accounts.forms import (UserCreationForm, UserChangeForm,
  13 + UserUpdateForm, UserForm, get_lists_choices,
  14 + ColabSetPasswordForm,
  15 + ColabPasswordChangeForm)
14 16 from colab.accounts import forms as accounts_forms
15 17 from colab.accounts.models import User
16 18 from colab.accounts.utils import mailman
17 19  
18 20  
  21 +class SetPasswordFormTestCase(TestCase):
  22 +
  23 + TEST_COLAB_APPS = {
  24 + 'test_plugin': {
  25 + 'password_validators': (
  26 + 'colab.accounts.tests.utils.password_validator',
  27 + )
  28 + }
  29 + }
  30 +
  31 + @property
  32 + def user(self):
  33 + return User.objects.create_user(username='test_user',
  34 + email='test@example.com')
  35 +
  36 + @property
  37 + def valid_form_data(self):
  38 + return {'new_password1': '12345',
  39 + 'new_password2': '12345'}
  40 +
  41 + def test_no_custom_validators(self):
  42 + form = ColabSetPasswordForm(self.user, data=self.valid_form_data)
  43 + self.assertTrue(form.is_valid(), True)
  44 +
  45 + @override_settings(COLAB_APPS=TEST_COLAB_APPS)
  46 + @patch('colab.accounts.tests.utils.password_validator')
  47 + def test_custom_validator(self, validator):
  48 + form = ColabSetPasswordForm(self.user, data=self.valid_form_data)
  49 + self.assertTrue(form.is_valid())
  50 + validator.assert_called_with('12345')
  51 +
  52 +
19 53 class FormTest(TestCase):
20 54  
21 55 def setUp(self):
... ... @@ -183,3 +217,121 @@ class FormTest(TestCase):
183 217 ('listB', u'listB (B)'),
184 218 ('listC', u'listC (C)'),
185 219 ('listD', u'listD (D)')])
  220 +
  221 +
  222 +class ChangePasswordFormTestCase(TestCase):
  223 +
  224 + TEST_COLAB_APPS = {
  225 + 'test_plugin': {
  226 + 'password_validators': (
  227 + 'colab.accounts.tests.utils.password_validator',
  228 + )
  229 + }
  230 + }
  231 +
  232 + @property
  233 + def user(self):
  234 + u = User.objects.create_user(username='test_user',
  235 + email='test@example.com')
  236 + u.set_password("123colab4")
  237 + return u
  238 +
  239 + @property
  240 + def valid_form_data(self):
  241 + return {'old_password': '123colab4',
  242 + 'new_password1': '12345',
  243 + 'new_password2': '12345'}
  244 +
  245 + def test_no_custom_validators(self):
  246 + form = ColabPasswordChangeForm(self.user, data=self.valid_form_data)
  247 + self.assertTrue(form.is_valid(), True)
  248 +
  249 + @override_settings(COLAB_APPS=TEST_COLAB_APPS)
  250 + @patch('colab.accounts.tests.utils.password_validator')
  251 + def test_custom_validator(self, validator):
  252 + form = ColabPasswordChangeForm(self.user, data=self.valid_form_data)
  253 + self.assertTrue(form.is_valid())
  254 + validator.assert_called_with('12345')
  255 +
  256 +
  257 +class UserCreationFormTestCase(TestCase):
  258 +
  259 + @classmethod
  260 + def setUpClass(cls):
  261 + cls.user = User.objects.create_user(username='user1234',
  262 + email='teste1234@example.com',
  263 + first_name='test_first_name',
  264 + last_name='test_last_name')
  265 +
  266 + cls.user.set_password("123colab4")
  267 + cls.user.save()
  268 +
  269 + def get_form_data(self, email, username='test_user',
  270 + password1='12345', password2='12345'):
  271 + return {
  272 + 'first_name': 'test_first_name',
  273 + 'last_name': 'test_last_name',
  274 + 'username': username,
  275 + 'email': email,
  276 + 'password1': password1,
  277 + 'password2': password2
  278 + }
  279 +
  280 + def test_clean_mail_error(self):
  281 + creation_form = UserCreationForm(
  282 + data=self.get_form_data('teste1234@example.com'))
  283 + self.assertFalse(creation_form.is_valid())
  284 + self.assertTrue('email' in creation_form.errors)
  285 + self.assertEqual(1, len(creation_form.errors))
  286 +
  287 + def test_clean_mail(self):
  288 + creation_form = UserCreationForm(
  289 + data=self.get_form_data('teste12345@example.com'))
  290 + self.assertTrue(creation_form.is_valid())
  291 +
  292 + def test_clean_username_error(self):
  293 + creation_form = UserCreationForm(
  294 + data=self.get_form_data('teste12345@example.com',
  295 + username='user1234'))
  296 + self.assertFalse(creation_form.is_valid())
  297 + self.assertTrue('username' in creation_form.errors)
  298 + self.assertEqual(1, len(creation_form.errors))
  299 +
  300 + def test_clean_username(self):
  301 + creation_form = UserCreationForm(
  302 + data=self.get_form_data('teste12345@example.com',
  303 + username='user12345'))
  304 + self.assertTrue(creation_form.is_valid())
  305 +
  306 + def test_clean_password2_empty_password1(self):
  307 + creation_form = UserCreationForm(
  308 + data=self.get_form_data('teste12345@example.com',
  309 + username='user12345',
  310 + password1=''))
  311 + self.assertFalse(creation_form.is_valid())
  312 + self.assertTrue('password1' in creation_form.errors)
  313 + self.assertEqual(1, len(creation_form.errors))
  314 +
  315 + def test_clean_password2_empty_password2(self):
  316 + creation_form = UserCreationForm(
  317 + data=self.get_form_data('teste12345@example.com',
  318 + username='user12345',
  319 + password2=''))
  320 + self.assertFalse(creation_form.is_valid())
  321 + self.assertTrue('password2' in creation_form.errors)
  322 +
  323 + def test_clean_password2_different_passwords(self):
  324 + creation_form = UserCreationForm(
  325 + data=self.get_form_data('teste12345@example.com',
  326 + username='user12345',
  327 + password1='1234'))
  328 + self.assertFalse(creation_form.is_valid())
  329 + self.assertTrue('password2' in creation_form.errors)
  330 + self.assertEqual(1, len(creation_form.errors))
  331 + self.assertEqual(1, len(creation_form.errors))
  332 +
  333 + def test_clean_password(self):
  334 + creation_form = UserCreationForm(
  335 + data=self.get_form_data('teste12345@example.com',
  336 + username='user12345'))
  337 + self.assertTrue(creation_form.is_valid())
... ...
colab/accounts/tests/utils.py 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +from django.forms import ValidationError
  2 +
  3 +
  4 +def password_validator(password):
  5 + raise ValidationError('Test error')
... ...
colab/accounts/urls.py
1 1  
2 2 from django.conf import settings
3 3 from django.conf.urls import patterns, url
  4 +from django.contrib.auth import views as auth_views
4 5  
5 6 from .views import (UserProfileDetailView, UserProfileUpdateView,
6 7 ManageUserSubscriptionsView)
7   -
8   -from colab.accounts import views
9   -from django.contrib.auth import views as auth_views
  8 +from .forms import ColabPasswordChangeForm, ColabSetPasswordForm
10 9  
11 10  
12 11 urlpatterns = patterns('',
... ... @@ -22,7 +21,8 @@ urlpatterns = patterns('',
22 21  
23 22 url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
24 23 auth_views.password_reset_confirm,
25   - {'template_name':'registration/password_reset_confirm_custom.html'},
  24 + {'template_name':'registration/password_reset_confirm_custom.html',
  25 + 'set_password_form': ColabSetPasswordForm},
26 26 name="password_reset_confirm"),
27 27  
28 28 url(r'^password-reset/?$', auth_views.password_reset,
... ... @@ -30,7 +30,8 @@ urlpatterns = patterns(&#39;&#39;,
30 30 name="password_reset"),
31 31  
32 32 url(r'^change-password/?$', auth_views.password_change,
33   - {'template_name':'registration/password_change_form_custom.html'},
  33 + {'template_name': 'registration/password_change_form_custom.html',
  34 + 'password_change_form': ColabPasswordChangeForm},
34 35 name='password_change'),
35 36  
36 37 url(r'^change-password-done/?$',
... ...
colab/utils/conf.py
... ... @@ -49,7 +49,7 @@ def _load_py_file(py_path, path):
49 49 sys.path.remove(path)
50 50  
51 51 py_setting = {var: getattr(py_settings, var) for var in dir(py_settings)
52   - if not var.startswith('__')}
  52 + if not var.startswith('_')}
53 53  
54 54 return py_setting
55 55  
... ... @@ -125,16 +125,7 @@ def load_colab_apps():
125 125  
126 126 app_label = app_name.split('.')[-1]
127 127 COLAB_APPS[app_label] = {}
128   - COLAB_APPS[app_label]['menu_title'] = py_settings_d.get('menu_title')
129   -
130   - fields = ['verbose_name', 'upstream', 'urls',
131   - 'menu_urls', 'middlewares', 'dependencies',
132   - 'context_processors', 'private_token', 'name', 'extra']
133   -
134   - for key in fields:
135   - value = py_settings_d.get(key)
136   - if value:
137   - COLAB_APPS[app_label][key] = value
  128 + COLAB_APPS[app_label] = py_settings_d
138 129  
139 130 return {'COLAB_APPS': COLAB_APPS}
140 131  
... ...
docs/source/plugindev.rst
... ... @@ -189,3 +189,32 @@ Example Usage:
189 189  
190 190 def get_last_updated_timestamp(self):
191 191 return TimeStampPlugin.get_last_updated_timestamp('TestPlugin')
  192 +
  193 +
  194 +Password Validation
  195 +-------------------
  196 +
  197 +Allows the plugin to define rules to set the password. The validators
  198 +are functions which receive the password as only argument and if it
  199 +doesn't match the desired rules raises a `ValidationError`. The message
  200 +sent in the validation error will be displayed to user in the HTML form.
  201 +
  202 +Example:
  203 +
  204 +.. code-block:: python
  205 +
  206 + ## myplugin/password_validators.py
  207 +
  208 + def has_uppercase_char(password):
  209 + for char in password:
  210 + if char.isupper():
  211 + return
  212 +
  213 + raise ValidationError('Password must have at least one upper case char')
  214 +
  215 + ## /etc/colab/plugins.d/myplugin.py
  216 +
  217 + password_validators = (
  218 + 'myplugin.password_validators.has_uppercase_char',
  219 + )
  220 +
... ...
docs/source/user.rst
... ... @@ -165,6 +165,15 @@ Declares the additional installed apps that this plugin depends on.
165 165 This doesn't automatically install the python dependecies, only add to django
166 166 apps.
167 167  
  168 +.. attribute:: password_validators
  169 +
  170 +A lista of functions to validade password in the moment it's set.
  171 +This allows plugins to define their own password validators. For
  172 +example if the proxied app requires the password to have at least
  173 +one upper case character it should provide a password validator
  174 +for that.
  175 +
  176 +
168 177 urls
169 178 ++++
170 179  
... ...