From e9631d3cee2cf23690cab16716f3310ae9c0df25 Mon Sep 17 00:00:00 2001 From: Sergio Oliveira Date: Thu, 26 Nov 2015 12:00:55 -0200 Subject: [PATCH] Added plugin ability to validate colab password --- colab/accounts/forms.py | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ colab/accounts/models.py | 2 +- colab/accounts/urls.py | 8 ++++---- colab/utils/conf.py | 13 ++----------- 4 files changed, 258 insertions(+), 16 deletions(-) diff --git a/colab/accounts/forms.py b/colab/accounts/forms.py index 42595ca..bab774f 100644 --- a/colab/accounts/forms.py +++ b/colab/accounts/forms.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +from collections import OrderedDict +from importlib import import_module + from django import forms from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.core.urlresolvers import reverse from django.utils.functional import lazy @@ -282,3 +286,250 @@ class UserChangeForm(forms.ModelForm): # This is done here, rather than on the field, because the # field does not have access to the initial value return self.initial["password"] + + +class AuthenticationForm(forms.Form): + """ + Base class for authenticating users. Extend this to get a form that accepts + username/password logins. + """ + username = forms.CharField(max_length=254) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) + + error_messages = { + 'invalid_login': _("Please enter a correct %(username)s and password. " + "Note that both fields may be case-sensitive."), + 'inactive': _("This account is inactive."), + } + + def __init__(self, request=None, *args, **kwargs): + """ + The 'request' parameter is set for custom auth use by subclasses. + The form data comes in via the standard 'data' kwarg. + """ + self.request = request + self.user_cache = None + super(AuthenticationForm, self).__init__(*args, **kwargs) + + # Set the label for the "username" field. + UserModel = get_user_model() + self.username_field = UserModel._meta.get_field( + UserModel.USERNAME_FIELD) + if self.fields['username'].label is None: + self.fields['username'].label = capfirst( + self.username_field.verbose_name) + + def clean(self): + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + + if username and password: + self.user_cache = authenticate(username=username, + password=password) + if self.user_cache is None: + raise forms.ValidationError( + self.error_messages['invalid_login'], + code='invalid_login', + params={'username': self.username_field.verbose_name}, + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data + + def confirm_login_allowed(self, user): + """ + Controls whether the given User may log in. This is a policy setting, + independent of end-user authentication. This default behavior is to + allow login by active users, and reject login by inactive users. + If the given user cannot log in, this method should raise a + ``forms.ValidationError``. + If the given user may log in, this method should return None. + """ + if not user.is_active: + raise forms.ValidationError( + self.error_messages['inactive'], + code='inactive', + ) + + def get_user_id(self): + if self.user_cache: + return self.user_cache.id + return None + + def get_user(self): + return self.user_cache + + +class PasswordResetForm(forms.Form): + email = forms.EmailField(label=_("Email"), max_length=254) + + def save(self, domain_override=None, + subject_template_name='registration/password_reset_subject.txt', + email_template_name='registration/password_reset_email.html', + use_https=False, token_generator=default_token_generator, + from_email=None, request=None, html_email_template_name=None): + """ + Generates a one-use only link for resetting password and sends to the + user. + """ + from django.core.mail import send_mail + UserModel = get_user_model() + email = self.cleaned_data["email"] + active_users = UserModel._default_manager.filter( + email__iexact=email, is_active=True) + for user in active_users: + # Make sure that no email is sent to a user that actually has + # a password marked as unusable + if not user.has_usable_password(): + continue + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + c = { + 'email': user.email, + 'domain': domain, + 'site_name': site_name, + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': 'https' if use_https else 'http', + } + subject = loader.render_to_string(subject_template_name, c) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + email = loader.render_to_string(email_template_name, c) + + if html_email_template_name: + html_email = loader.render_to_string(html_email_template_name, + c) + else: + html_email = None + send_mail(subject, email, from_email, [user.email], + html_message=html_email) + + +class SetPasswordForm(forms.Form): + """ + A form that lets a user change set their password without entering the old + password + """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } + new_password1 = forms.CharField(label=_("New password"), + widget=forms.PasswordInput) + new_password2 = forms.CharField(label=_("New password confirmation"), + widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(SetPasswordForm, self).__init__(*args, **kwargs) + + def clean_new_password1(self): + password = self.cleaned_data.get('new_password1') + + for app in settings.COLAB_APPS.values(): + if 'password_validators' in app: + for validator_path in app.get('password_validators'): + module_path, func_name = validator_path.rsplit('.', 1) + module = import_module(module_path) + validator_func = getattr(module, func_name, None) + if validator_func: + validator_func(password) + + return password + + def clean_new_password2(self): + password1 = self.cleaned_data.get('new_password1') + password2 = self.cleaned_data.get('new_password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + + def save(self, commit=True): + self.user.set_password(self.cleaned_data['new_password1']) + if commit: + self.user.save() + return self.user + + +class PasswordChangeForm(SetPasswordForm): + """ + A form that lets a user change their password by entering their old + password. + """ + error_messages = dict(SetPasswordForm.error_messages, **{ + 'password_incorrect': _("Your old password was entered incorrectly. " + "Please enter it again."), + }) + old_password = forms.CharField(label=_("Old password"), + widget=forms.PasswordInput) + + def clean_old_password(self): + """ + Validates that the old_password field is correct. + """ + old_password = self.cleaned_data["old_password"] + if not self.user.check_password(old_password): + raise forms.ValidationError( + self.error_messages['password_incorrect'], + code='password_incorrect', + ) + return old_password + +PasswordChangeForm.base_fields = OrderedDict( + (k, PasswordChangeForm.base_fields[k]) + for k in ['old_password', 'new_password1', 'new_password2'] +) + + +class AdminPasswordChangeForm(forms.Form): + """ + A form used to change the password of a user in the admin interface. + """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } + password1 = forms.CharField(label=_("Password"), + widget=forms.PasswordInput) + password2 = forms.CharField(label=_("Password (again)"), + widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): + self.user = user + super(AdminPasswordChangeForm, self).__init__(*args, **kwargs) + + def clean_password2(self): + password1 = self.cleaned_data.get('password1') + password2 = self.cleaned_data.get('password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + + def save(self, commit=True): + """ + Saves the new password. + """ + self.user.set_password(self.cleaned_data["password1"]) + if commit: + self.user.save() + return self.user + + def _get_changed_data(self): + data = super(AdminPasswordChangeForm, self).changed_data + for name in self.fields.keys(): + if name not in data: + return [] + return ['password'] diff --git a/colab/accounts/models.py b/colab/accounts/models.py index a6ca51f..ac92406 100644 --- a/colab/accounts/models.py +++ b/colab/accounts/models.py @@ -2,9 +2,9 @@ import urlparse -from django.db import models from django.contrib.auth.models import AbstractUser, UserManager from django.core.urlresolvers import reverse +from django.db import models from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ diff --git a/colab/accounts/urls.py b/colab/accounts/urls.py index 3ffad03..de4b0bc 100644 --- a/colab/accounts/urls.py +++ b/colab/accounts/urls.py @@ -1,12 +1,11 @@ from django.conf import settings from django.conf.urls import patterns, url +from django.contrib.auth import views as auth_views from .views import (UserProfileDetailView, UserProfileUpdateView, ManageUserSubscriptionsView) - -from colab.accounts import views -from django.contrib.auth import views as auth_views +from .forms import PasswordChangeForm urlpatterns = patterns('', @@ -30,7 +29,8 @@ urlpatterns = patterns('', name="password_reset"), url(r'^change-password/?$', auth_views.password_change, - {'template_name':'registration/password_change_form_custom.html'}, + {'template_name': 'registration/password_change_form_custom.html', + 'password_change_form': PasswordChangeForm}, name='password_change'), url(r'^change-password-done/?$', diff --git a/colab/utils/conf.py b/colab/utils/conf.py index 65aba93..f103217 100644 --- a/colab/utils/conf.py +++ b/colab/utils/conf.py @@ -49,7 +49,7 @@ def _load_py_file(py_path, path): sys.path.remove(path) py_setting = {var: getattr(py_settings, var) for var in dir(py_settings) - if not var.startswith('__')} + if not var.startswith('_')} return py_setting @@ -125,16 +125,7 @@ def load_colab_apps(): app_label = app_name.split('.')[-1] COLAB_APPS[app_label] = {} - COLAB_APPS[app_label]['menu_title'] = py_settings_d.get('menu_title') - - fields = ['verbose_name', 'upstream', 'urls', - 'menu_urls', 'middlewares', 'dependencies', - 'context_processors', 'private_token', 'name', 'extra'] - - for key in fields: - value = py_settings_d.get(key) - if value: - COLAB_APPS[app_label][key] = value + COLAB_APPS[app_label] = py_settings_d return {'COLAB_APPS': COLAB_APPS} -- libgit2 0.21.2