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 | 1 | # -*- coding: utf-8 -*- |
| 2 | 2 | |
| 3 | +from collections import OrderedDict | |
| 4 | +from importlib import import_module | |
| 5 | + | |
| 3 | 6 | from django import forms |
| 4 | 7 | from django.conf import settings |
| 5 | 8 | from django.contrib.auth import get_user_model |
| 9 | +from django.contrib.auth.tokens import default_token_generator | |
| 6 | 10 | from django.contrib.auth.forms import ReadOnlyPasswordHashField |
| 7 | 11 | from django.core.urlresolvers import reverse |
| 8 | 12 | from django.utils.functional import lazy |
| ... | ... | @@ -282,3 +286,250 @@ class UserChangeForm(forms.ModelForm): |
| 282 | 286 | # This is done here, rather than on the field, because the |
| 283 | 287 | # field does not have access to the initial value |
| 284 | 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 | 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/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 PasswordChangeForm | |
| 10 | 9 | |
| 11 | 10 | |
| 12 | 11 | urlpatterns = patterns('', |
| ... | ... | @@ -30,7 +29,8 @@ urlpatterns = patterns('', |
| 30 | 29 | name="password_reset"), |
| 31 | 30 | |
| 32 | 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 | 34 | name='password_change'), |
| 35 | 35 | |
| 36 | 36 | 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 | ... | ... |