From a1889397a9f0e6a35189de455098b4c70923e561 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 30 Jul 2013 08:24:13 -0400 Subject: [PATCH] Fixed #12103 -- Added AuthenticationForm.confirm_login_allowed to allow customizing the logic policy. Thanks ejucovy and lasko for work on the patch. --- django/contrib/auth/forms.py | 25 ++++++++++++++---- django/contrib/auth/tests/test_forms.py | 35 +++++++++++++++++++++++++ docs/releases/1.7.txt | 4 +++ docs/topics/auth/default.txt | 34 ++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index bf58d0e903..e3f03cc536 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -191,13 +191,28 @@ class AuthenticationForm(forms.Form): code='invalid_login', params={'username': self.username_field.verbose_name}, ) - elif not self.user_cache.is_active: - raise forms.ValidationError( - self.error_messages['inactive'], - code='inactive', - ) + 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 diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py index 0b998105af..154960c8da 100644 --- a/django/contrib/auth/tests/test_forms.py +++ b/django/contrib/auth/tests/test_forms.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import os +from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, @@ -131,6 +132,40 @@ class AuthenticationFormTest(TestCase): self.assertEqual(form.non_field_errors(), [force_text(form.error_messages['inactive'])]) + def test_custom_login_allowed_policy(self): + # The user is inactive, but our custom form policy allows him to log in. + data = { + 'username': 'inactive', + 'password': 'password', + } + + class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm): + def confirm_login_allowed(self, user): + pass + + form = AuthenticationFormWithInactiveUsersOkay(None, data) + self.assertTrue(form.is_valid()) + + # If we want to disallow some logins according to custom logic, + # we should raise a django.forms.ValidationError in the form. + class PickyAuthenticationForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if user.username == "inactive": + raise forms.ValidationError(_("This user is disallowed.")) + raise forms.ValidationError(_("Sorry, nobody's allowed in.")) + + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ['This user is disallowed.']) + + data = { + 'username': 'testclient', + 'password': 'password', + } + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."]) + def test_success(self): # The success case data = { diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 0951cd3058..b75fd89f4c 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -101,6 +101,10 @@ Minor features :class:`~django.middleware.http.ConditionalGetMiddleware` to handle conditional ``GET`` requests for sitemaps which set ``lastmod``. +* You can override the new :meth:`AuthenticationForm.confirm_login_allowed() + ` method + to more easily customize the login policy. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index b7f679bf28..25fe262b56 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -959,6 +959,40 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: Takes ``request`` as its first positional argument, which is stored on the form instance for use by sub-classes. + .. method:: confirm_login_allowed(user) + + .. versionadded:: 1.7 + + By default, ``AuthenticationForm`` rejects users whose ``is_active`` flag + is set to ``False``. You may override this behavior with a custom policy to + determine which users can log in. Do this with a custom form that subclasses + ``AuthenticationForm`` and overrides the ``confirm_login_allowed`` method. + This method should raise a :exc:`~django.core.exceptions.ValidationError` + if the given user may not log in. + + For example, to allow all users to log in, regardless of "active" status:: + + from django.contrib.auth.forms import AuthenticationForm + + class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm): + def confirm_login_allowed(self, user): + pass + + Or to allow only some active users to log in:: + + class PickyAuthenticationForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if not user.is_active: + raise forms.ValidationError( + _("This account is inactive."), + code='inactive', + ) + if user.username.startswith('b'): + raise forms.ValidationError( + _("Sorry, accounts starting with 'b' aren't welcome here."), + code='no_b_users', + ) + .. class:: PasswordChangeForm A form for allowing a user to change their password.