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 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 2
  3 +from importlib import import_module
  4 +
3 from django import forms 5 from django import forms
4 from django.conf import settings 6 from django.conf import settings
5 from django.contrib.auth import get_user_model 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 from django.core.urlresolvers import reverse 10 from django.core.urlresolvers import reverse
8 from django.utils.functional import lazy 11 from django.utils.functional import lazy
9 from django.utils.translation import ugettext_lazy as _ 12 from django.utils.translation import ugettext_lazy as _
@@ -160,7 +163,41 @@ class ListsForm(forms.Form): @@ -160,7 +163,41 @@ class ListsForm(forms.Form):
160 choices=lazy(get_lists_choices, list)()) 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 A form that creates a user, with no privileges, from the given username and 202 A form that creates a user, with no privileges, from the given username and
166 password. 203 password.
@@ -231,6 +268,8 @@ class UserCreationForm(UserForm): @@ -231,6 +268,8 @@ class UserCreationForm(UserForm):
231 self.error_messages['password_mismatch'], 268 self.error_messages['password_mismatch'],
232 code='password_mismatch', 269 code='password_mismatch',
233 ) 270 )
  271 +
  272 + super(UserCreationForm, self).clean_password2()
234 return password2 273 return password2
235 274
236 def save(self, commit=True): 275 def save(self, commit=True):
@@ -282,3 +321,11 @@ class UserChangeForm(forms.ModelForm): @@ -282,3 +321,11 @@ class UserChangeForm(forms.ModelForm):
282 # This is done here, rather than on the field, because the 321 # This is done here, rather than on the field, because the
283 # field does not have access to the initial value 322 # field does not have access to the initial value
284 return self.initial["password"] 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,9 +2,9 @@
2 2
3 import urlparse 3 import urlparse
4 4
5 -from django.db import models  
6 from django.contrib.auth.models import AbstractUser, UserManager 5 from django.contrib.auth.models import AbstractUser, UserManager
7 from django.core.urlresolvers import reverse 6 from django.core.urlresolvers import reverse
  7 +from django.db import models
8 from django.utils.crypto import get_random_string 8 from django.utils.crypto import get_random_string
9 from django.utils.translation import ugettext_lazy as _ 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,16 +6,50 @@ Objective: Test parameters, and behavior.
6 import datetime 6 import datetime
7 from mock import patch 7 from mock import patch
8 8
9 -from django.test import TestCase 9 +from django.test import TestCase, override_settings
10 from django.core.urlresolvers import reverse 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 from colab.accounts import forms as accounts_forms 16 from colab.accounts import forms as accounts_forms
15 from colab.accounts.models import User 17 from colab.accounts.models import User
16 from colab.accounts.utils import mailman 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 class FormTest(TestCase): 53 class FormTest(TestCase):
20 54
21 def setUp(self): 55 def setUp(self):
@@ -183,3 +217,121 @@ class FormTest(TestCase): @@ -183,3 +217,121 @@ class FormTest(TestCase):
183 ('listB', u'listB (B)'), 217 ('listB', u'listB (B)'),
184 ('listC', u'listC (C)'), 218 ('listC', u'listC (C)'),
185 ('listD', u'listD (D)')]) 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 @@ @@ -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 from django.conf import settings 2 from django.conf import settings
3 from django.conf.urls import patterns, url 3 from django.conf.urls import patterns, url
  4 +from django.contrib.auth import views as auth_views
4 5
5 from .views import (UserProfileDetailView, UserProfileUpdateView, 6 from .views import (UserProfileDetailView, UserProfileUpdateView,
6 ManageUserSubscriptionsView) 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 urlpatterns = patterns('', 11 urlpatterns = patterns('',
@@ -22,7 +21,8 @@ urlpatterns = patterns('', @@ -22,7 +21,8 @@ urlpatterns = patterns('',
22 21
23 url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', 22 url(r'^password-reset-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
24 auth_views.password_reset_confirm, 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 name="password_reset_confirm"), 26 name="password_reset_confirm"),
27 27
28 url(r'^password-reset/?$', auth_views.password_reset, 28 url(r'^password-reset/?$', auth_views.password_reset,
@@ -30,7 +30,8 @@ urlpatterns = patterns(&#39;&#39;, @@ -30,7 +30,8 @@ urlpatterns = patterns(&#39;&#39;,
30 name="password_reset"), 30 name="password_reset"),
31 31
32 url(r'^change-password/?$', auth_views.password_change, 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 name='password_change'), 35 name='password_change'),
35 36
36 url(r'^change-password-done/?$', 37 url(r'^change-password-done/?$',
colab/utils/conf.py
@@ -49,7 +49,7 @@ def _load_py_file(py_path, path): @@ -49,7 +49,7 @@ def _load_py_file(py_path, path):
49 sys.path.remove(path) 49 sys.path.remove(path)
50 50
51 py_setting = {var: getattr(py_settings, var) for var in dir(py_settings) 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 return py_setting 54 return py_setting
55 55
@@ -125,16 +125,7 @@ def load_colab_apps(): @@ -125,16 +125,7 @@ def load_colab_apps():
125 125
126 app_label = app_name.split('.')[-1] 126 app_label = app_name.split('.')[-1]
127 COLAB_APPS[app_label] = {} 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 return {'COLAB_APPS': COLAB_APPS} 130 return {'COLAB_APPS': COLAB_APPS}
140 131
docs/source/plugindev.rst
@@ -189,3 +189,32 @@ Example Usage: @@ -189,3 +189,32 @@ Example Usage:
189 189
190 def get_last_updated_timestamp(self): 190 def get_last_updated_timestamp(self):
191 return TimeStampPlugin.get_last_updated_timestamp('TestPlugin') 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,6 +165,15 @@ Declares the additional installed apps that this plugin depends on.
165 This doesn't automatically install the python dependecies, only add to django 165 This doesn't automatically install the python dependecies, only add to django
166 apps. 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 urls 177 urls
169 ++++ 178 ++++
170 179