Commit e9631d3cee2cf23690cab16716f3310ae9c0df25
1 parent
e24dc879
Exists in
master
and in
4 other branches
Added plugin ability to validate colab password
Showing
4 changed files
with
258 additions
and
16 deletions
Show diff stats
colab/accounts/forms.py
1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
2 | 2 | ||
3 | +from collections import OrderedDict | ||
4 | +from importlib import import_module | ||
5 | + | ||
3 | from django import forms | 6 | from django import forms |
4 | from django.conf import settings | 7 | from django.conf import settings |
5 | from django.contrib.auth import get_user_model | 8 | from django.contrib.auth import get_user_model |
9 | +from django.contrib.auth.tokens import default_token_generator | ||
6 | from django.contrib.auth.forms import ReadOnlyPasswordHashField | 10 | from django.contrib.auth.forms import ReadOnlyPasswordHashField |
7 | from django.core.urlresolvers import reverse | 11 | from django.core.urlresolvers import reverse |
8 | from django.utils.functional import lazy | 12 | from django.utils.functional import lazy |
@@ -282,3 +286,250 @@ class UserChangeForm(forms.ModelForm): | @@ -282,3 +286,250 @@ class UserChangeForm(forms.ModelForm): | ||
282 | # This is done here, rather than on the field, because the | 286 | # This is done here, rather than on the field, because the |
283 | # field does not have access to the initial value | 287 | # field does not have access to the initial value |
284 | return self.initial["password"] | 288 | return self.initial["password"] |
289 | + | ||
290 | + | ||
291 | +class AuthenticationForm(forms.Form): | ||
292 | + """ | ||
293 | + Base class for authenticating users. Extend this to get a form that accepts | ||
294 | + username/password logins. | ||
295 | + """ | ||
296 | + username = forms.CharField(max_length=254) | ||
297 | + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) | ||
298 | + | ||
299 | + error_messages = { | ||
300 | + 'invalid_login': _("Please enter a correct %(username)s and password. " | ||
301 | + "Note that both fields may be case-sensitive."), | ||
302 | + 'inactive': _("This account is inactive."), | ||
303 | + } | ||
304 | + | ||
305 | + def __init__(self, request=None, *args, **kwargs): | ||
306 | + """ | ||
307 | + The 'request' parameter is set for custom auth use by subclasses. | ||
308 | + The form data comes in via the standard 'data' kwarg. | ||
309 | + """ | ||
310 | + self.request = request | ||
311 | + self.user_cache = None | ||
312 | + super(AuthenticationForm, self).__init__(*args, **kwargs) | ||
313 | + | ||
314 | + # Set the label for the "username" field. | ||
315 | + UserModel = get_user_model() | ||
316 | + self.username_field = UserModel._meta.get_field( | ||
317 | + UserModel.USERNAME_FIELD) | ||
318 | + if self.fields['username'].label is None: | ||
319 | + self.fields['username'].label = capfirst( | ||
320 | + self.username_field.verbose_name) | ||
321 | + | ||
322 | + def clean(self): | ||
323 | + username = self.cleaned_data.get('username') | ||
324 | + password = self.cleaned_data.get('password') | ||
325 | + | ||
326 | + if username and password: | ||
327 | + self.user_cache = authenticate(username=username, | ||
328 | + password=password) | ||
329 | + if self.user_cache is None: | ||
330 | + raise forms.ValidationError( | ||
331 | + self.error_messages['invalid_login'], | ||
332 | + code='invalid_login', | ||
333 | + params={'username': self.username_field.verbose_name}, | ||
334 | + ) | ||
335 | + else: | ||
336 | + self.confirm_login_allowed(self.user_cache) | ||
337 | + | ||
338 | + return self.cleaned_data | ||
339 | + | ||
340 | + def confirm_login_allowed(self, user): | ||
341 | + """ | ||
342 | + Controls whether the given User may log in. This is a policy setting, | ||
343 | + independent of end-user authentication. This default behavior is to | ||
344 | + allow login by active users, and reject login by inactive users. | ||
345 | + If the given user cannot log in, this method should raise a | ||
346 | + ``forms.ValidationError``. | ||
347 | + If the given user may log in, this method should return None. | ||
348 | + """ | ||
349 | + if not user.is_active: | ||
350 | + raise forms.ValidationError( | ||
351 | + self.error_messages['inactive'], | ||
352 | + code='inactive', | ||
353 | + ) | ||
354 | + | ||
355 | + def get_user_id(self): | ||
356 | + if self.user_cache: | ||
357 | + return self.user_cache.id | ||
358 | + return None | ||
359 | + | ||
360 | + def get_user(self): | ||
361 | + return self.user_cache | ||
362 | + | ||
363 | + | ||
364 | +class PasswordResetForm(forms.Form): | ||
365 | + email = forms.EmailField(label=_("Email"), max_length=254) | ||
366 | + | ||
367 | + def save(self, domain_override=None, | ||
368 | + subject_template_name='registration/password_reset_subject.txt', | ||
369 | + email_template_name='registration/password_reset_email.html', | ||
370 | + use_https=False, token_generator=default_token_generator, | ||
371 | + from_email=None, request=None, html_email_template_name=None): | ||
372 | + """ | ||
373 | + Generates a one-use only link for resetting password and sends to the | ||
374 | + user. | ||
375 | + """ | ||
376 | + from django.core.mail import send_mail | ||
377 | + UserModel = get_user_model() | ||
378 | + email = self.cleaned_data["email"] | ||
379 | + active_users = UserModel._default_manager.filter( | ||
380 | + email__iexact=email, is_active=True) | ||
381 | + for user in active_users: | ||
382 | + # Make sure that no email is sent to a user that actually has | ||
383 | + # a password marked as unusable | ||
384 | + if not user.has_usable_password(): | ||
385 | + continue | ||
386 | + if not domain_override: | ||
387 | + current_site = get_current_site(request) | ||
388 | + site_name = current_site.name | ||
389 | + domain = current_site.domain | ||
390 | + else: | ||
391 | + site_name = domain = domain_override | ||
392 | + c = { | ||
393 | + 'email': user.email, | ||
394 | + 'domain': domain, | ||
395 | + 'site_name': site_name, | ||
396 | + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), | ||
397 | + 'user': user, | ||
398 | + 'token': token_generator.make_token(user), | ||
399 | + 'protocol': 'https' if use_https else 'http', | ||
400 | + } | ||
401 | + subject = loader.render_to_string(subject_template_name, c) | ||
402 | + # Email subject *must not* contain newlines | ||
403 | + subject = ''.join(subject.splitlines()) | ||
404 | + email = loader.render_to_string(email_template_name, c) | ||
405 | + | ||
406 | + if html_email_template_name: | ||
407 | + html_email = loader.render_to_string(html_email_template_name, | ||
408 | + c) | ||
409 | + else: | ||
410 | + html_email = None | ||
411 | + send_mail(subject, email, from_email, [user.email], | ||
412 | + html_message=html_email) | ||
413 | + | ||
414 | + | ||
415 | +class SetPasswordForm(forms.Form): | ||
416 | + """ | ||
417 | + A form that lets a user change set their password without entering the old | ||
418 | + password | ||
419 | + """ | ||
420 | + error_messages = { | ||
421 | + 'password_mismatch': _("The two password fields didn't match."), | ||
422 | + } | ||
423 | + new_password1 = forms.CharField(label=_("New password"), | ||
424 | + widget=forms.PasswordInput) | ||
425 | + new_password2 = forms.CharField(label=_("New password confirmation"), | ||
426 | + widget=forms.PasswordInput) | ||
427 | + | ||
428 | + def __init__(self, user, *args, **kwargs): | ||
429 | + self.user = user | ||
430 | + super(SetPasswordForm, self).__init__(*args, **kwargs) | ||
431 | + | ||
432 | + def clean_new_password1(self): | ||
433 | + password = self.cleaned_data.get('new_password1') | ||
434 | + | ||
435 | + for app in settings.COLAB_APPS.values(): | ||
436 | + if 'password_validators' in app: | ||
437 | + for validator_path in app.get('password_validators'): | ||
438 | + module_path, func_name = validator_path.rsplit('.', 1) | ||
439 | + module = import_module(module_path) | ||
440 | + validator_func = getattr(module, func_name, None) | ||
441 | + if validator_func: | ||
442 | + validator_func(password) | ||
443 | + | ||
444 | + return password | ||
445 | + | ||
446 | + def clean_new_password2(self): | ||
447 | + password1 = self.cleaned_data.get('new_password1') | ||
448 | + password2 = self.cleaned_data.get('new_password2') | ||
449 | + if password1 and password2: | ||
450 | + if password1 != password2: | ||
451 | + raise forms.ValidationError( | ||
452 | + self.error_messages['password_mismatch'], | ||
453 | + code='password_mismatch', | ||
454 | + ) | ||
455 | + return password2 | ||
456 | + | ||
457 | + def save(self, commit=True): | ||
458 | + self.user.set_password(self.cleaned_data['new_password1']) | ||
459 | + if commit: | ||
460 | + self.user.save() | ||
461 | + return self.user | ||
462 | + | ||
463 | + | ||
464 | +class PasswordChangeForm(SetPasswordForm): | ||
465 | + """ | ||
466 | + A form that lets a user change their password by entering their old | ||
467 | + password. | ||
468 | + """ | ||
469 | + error_messages = dict(SetPasswordForm.error_messages, **{ | ||
470 | + 'password_incorrect': _("Your old password was entered incorrectly. " | ||
471 | + "Please enter it again."), | ||
472 | + }) | ||
473 | + old_password = forms.CharField(label=_("Old password"), | ||
474 | + widget=forms.PasswordInput) | ||
475 | + | ||
476 | + def clean_old_password(self): | ||
477 | + """ | ||
478 | + Validates that the old_password field is correct. | ||
479 | + """ | ||
480 | + old_password = self.cleaned_data["old_password"] | ||
481 | + if not self.user.check_password(old_password): | ||
482 | + raise forms.ValidationError( | ||
483 | + self.error_messages['password_incorrect'], | ||
484 | + code='password_incorrect', | ||
485 | + ) | ||
486 | + return old_password | ||
487 | + | ||
488 | +PasswordChangeForm.base_fields = OrderedDict( | ||
489 | + (k, PasswordChangeForm.base_fields[k]) | ||
490 | + for k in ['old_password', 'new_password1', 'new_password2'] | ||
491 | +) | ||
492 | + | ||
493 | + | ||
494 | +class AdminPasswordChangeForm(forms.Form): | ||
495 | + """ | ||
496 | + A form used to change the password of a user in the admin interface. | ||
497 | + """ | ||
498 | + error_messages = { | ||
499 | + 'password_mismatch': _("The two password fields didn't match."), | ||
500 | + } | ||
501 | + password1 = forms.CharField(label=_("Password"), | ||
502 | + widget=forms.PasswordInput) | ||
503 | + password2 = forms.CharField(label=_("Password (again)"), | ||
504 | + widget=forms.PasswordInput) | ||
505 | + | ||
506 | + def __init__(self, user, *args, **kwargs): | ||
507 | + self.user = user | ||
508 | + super(AdminPasswordChangeForm, self).__init__(*args, **kwargs) | ||
509 | + | ||
510 | + def clean_password2(self): | ||
511 | + password1 = self.cleaned_data.get('password1') | ||
512 | + password2 = self.cleaned_data.get('password2') | ||
513 | + if password1 and password2: | ||
514 | + if password1 != password2: | ||
515 | + raise forms.ValidationError( | ||
516 | + self.error_messages['password_mismatch'], | ||
517 | + code='password_mismatch', | ||
518 | + ) | ||
519 | + return password2 | ||
520 | + | ||
521 | + def save(self, commit=True): | ||
522 | + """ | ||
523 | + Saves the new password. | ||
524 | + """ | ||
525 | + self.user.set_password(self.cleaned_data["password1"]) | ||
526 | + if commit: | ||
527 | + self.user.save() | ||
528 | + return self.user | ||
529 | + | ||
530 | + def _get_changed_data(self): | ||
531 | + data = super(AdminPasswordChangeForm, self).changed_data | ||
532 | + for name in self.fields.keys(): | ||
533 | + if name not in data: | ||
534 | + return [] | ||
535 | + return ['password'] |
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/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 PasswordChangeForm |
10 | 9 | ||
11 | 10 | ||
12 | urlpatterns = patterns('', | 11 | urlpatterns = patterns('', |
@@ -30,7 +29,8 @@ urlpatterns = patterns('', | @@ -30,7 +29,8 @@ urlpatterns = patterns('', | ||
30 | name="password_reset"), | 29 | name="password_reset"), |
31 | 30 | ||
32 | url(r'^change-password/?$', auth_views.password_change, | 31 | url(r'^change-password/?$', auth_views.password_change, |
33 | - {'template_name':'registration/password_change_form_custom.html'}, | 32 | + {'template_name': 'registration/password_change_form_custom.html', |
33 | + 'password_change_form': PasswordChangeForm}, | ||
34 | name='password_change'), | 34 | name='password_change'), |
35 | 35 | ||
36 | url(r'^change-password-done/?$', | 36 | 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 |