Commit 91d859bb2708608fad75a8bab13dbe9952715e9a
Exists in
master
and in
3 other branches
Merge pull request #124 from colab/validate-passwd
Validate passwd
Showing
8 changed files
with
256 additions
and
22 deletions
Show diff stats
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/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('', | @@ -30,7 +30,8 @@ urlpatterns = patterns('', | ||
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 |