Fixed #25232 -- Made ModelBackend/RemoteUserBackend reject inactive users.

This commit is contained in:
Alexander Gaevsky 2016-02-05 16:46:19 +02:00 committed by Tim Graham
parent 1555d50ea4
commit e0a3d93730
11 changed files with 216 additions and 30 deletions

View File

@ -15,12 +15,21 @@ class ModelBackend(object):
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
if user.check_password(password):
return user
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
def user_can_authenticate(self, user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None
def _get_user_permissions(self, user_obj):
return user_obj.user_permissions.all()
@ -90,9 +99,15 @@ class ModelBackend(object):
def get_user(self, user_id):
UserModel = get_user_model()
try:
return UserModel._default_manager.get(pk=user_id)
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
class AllowAllUsersModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True
class RemoteUserBackend(ModelBackend):
@ -140,7 +155,7 @@ class RemoteUserBackend(ModelBackend):
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
pass
return user
return user if self.user_can_authenticate(user) else None
def clean_username(self, username):
"""
@ -158,3 +173,8 @@ class RemoteUserBackend(ModelBackend):
By default, returns the user unmodified.
"""
return user
class AllowAllUsersRemoteUserBackend(RemoteUserBackend):
def user_can_authenticate(self, user):
return True

View File

@ -64,9 +64,20 @@ remote users. These interfaces work with users stored in the database
regardless of ``AUTHENTICATION_BACKENDS``.
.. note::
Since the ``RemoteUserBackend`` inherits from ``ModelBackend``, you will
still have all of the same permissions checking that is implemented in
``ModelBackend``.
Since the ``RemoteUserBackend`` inherits from ``ModelBackend``, you will
still have all of the same permissions checking that is implemented in
``ModelBackend``.
Users with :attr:`is_active=False
<django.contrib.auth.models.User.is_active>` won't be allowed to
authenticate. Use
:class:`~django.contrib.auth.backends.AllowAllUsersRemoteUserBackend` if
you want to allow them to.
.. versionchanged:: 1.10
In older versions, inactive users weren't rejected as described above.
If your authentication mechanism uses a custom HTTP header and not
``REMOTE_USER``, you can subclass ``RemoteUserMiddleware`` and set the

View File

@ -76,15 +76,26 @@ Fields
This doesn't necessarily control whether or not the user can log in.
Authentication backends aren't required to check for the ``is_active``
flag, and the default backends do not. If you want to reject a login
based on ``is_active`` being ``False``, it's up to you to check that in
your own login view or a custom authentication backend. However, the
flag but the default backend
(:class:`~django.contrib.auth.backends.ModelBackend`) and the
:class:`~django.contrib.auth.backends.RemoteUserBackend` do. You can
use :class:`~django.contrib.auth.backends.AllowAllUsersModelBackend`
or :class:`~django.contrib.auth.backends.AllowAllUsersRemoteUserBackend`
if you want to allow inactive users to login. In this case, you'll also
want to customize the
:class:`~django.contrib.auth.forms.AuthenticationForm` used by the
:func:`~django.contrib.auth.views.login` view (which is the default)
*does* perform this check, as do the permission-checking methods such
as :meth:`~django.contrib.auth.models.User.has_perm` and the
authentication in the Django admin. All of those functions/methods will
return ``False`` for inactive users.
:func:`~django.contrib.auth.views.login` view as it rejects inactive
users. Be aware that the permission-checking methods such as
:meth:`~django.contrib.auth.models.User.has_perm` and the
authentication in the Django admin all return ``False`` for inactive
users.
.. versionchanged:: 1.10
In older versions,
:class:`~django.contrib.auth.backends.ModelBackend` and
:class:`~django.contrib.auth.backends.RemoteUserBackend` allowed
inactive users to authenticate.
.. attribute:: is_superuser
@ -488,6 +499,32 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
Returns whether the ``user_obj`` has any permissions on the app
``app_label``.
.. method:: ModelBackend.user_can_authenticate()
.. versionadded:: 1.10
Returns whether the user is allowed to authenticate. To match the
behavior of :class:`~django.contrib.auth.forms.AuthenticationForm`
which :meth:`prohibits inactive users from logging in
<django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>`,
this method returns ``False`` for users with :attr:`is_active=False
<django.contrib.auth.models.User.is_active>`. Custom user models that
don't have an :attr:`~django.contrib.auth.models.CustomUser.is_active`
field are allowed.
.. class:: AllowAllUsersModelBackend
.. versionadded:: 1.10
Same as :class:`ModelBackend` except that it doesn't reject inactive users
because :meth:`~ModelBackend.user_can_authenticate` always returns ``True``.
When using this backend, you'll likely want to customize the
:class:`~django.contrib.auth.forms.AuthenticationForm` used by the
:func:`~django.contrib.auth.views.login` view by overriding the
:meth:`~django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed`
method as it rejects inactive users.
.. class:: RemoteUserBackend
Use this backend to take advantage of external-to-Django-handled
@ -529,3 +566,21 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
new user is created, and can be used to perform custom setup actions, such
as setting the user's groups based on attributes in an LDAP directory.
Returns the user object.
.. method:: RemoteUserBackend.user_can_authenticate()
.. versionadded:: 1.10
Returns whether the user is allowed to authenticate. This method returns
``False`` for users with :attr:`is_active=False
<django.contrib.auth.models.User.is_active>`. Custom user models that don't
have an :attr:`~django.contrib.auth.models.CustomUser.is_active` field are
allowed.
.. class:: AllowAllUsersRemoteUserBackend
.. versionadded:: 1.10
Same as :class:`RemoteUserBackend` except that it doesn't reject inactive
users because :attr:`~RemoteUserBackend.user_can_authenticate` always
returns ``True``.

View File

@ -669,6 +669,15 @@ Miscellaneous
calling ``Command.execute()``, pass the command object as the first argument
to ``call_command()``.
* :class:`~django.contrib.auth.backends.ModelBackend` and
:class:`~django.contrib.auth.backends.RemoteUserBackend` now reject inactive
users. This means that inactive users can't login and will be logged
out if they are switched from ``is_active=True`` to ``False``. If you need
the previous behavior, use the new
:class:`~django.contrib.auth.backends.AllowAllUsersModelBackend` or
:class:`~django.contrib.auth.backends.AllowAllUsersRemoteUserBackend`
in :setting:`AUTHENTICATION_BACKENDS` instead.
.. _deprecated-features-1.10:
Features deprecated in 1.10

View File

@ -235,10 +235,17 @@ for example, to control anonymous access.
Authorization for inactive users
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An inactive user is a one that is authenticated but has its attribute
``is_active`` set to ``False``. However this does not mean they are not
authorized to do anything. For example they are allowed to activate their
account.
An inactive user is a one that has its
:attr:`~django.contrib.auth.models.User.is_active` field set to ``False``. The
:class:`~django.contrib.auth.backends.ModelBackend` and
:class:`~django.contrib.auth.backends.RemoteUserBackend` authentication
backends prohibits these users from authenticating. If a custom user model
doesn't have an :attr:`~django.contrib.auth.models.CustomUser.is_active` field,
all users will be allowed to authenticate.
You can use :class:`~django.contrib.auth.backends.AllowAllUsersModelBackend`
or :class:`~django.contrib.auth.backends.AllowAllUsersRemoteUserBackend` if you
want to allow inactive users to authenticate.
The support for anonymous users in the permission system allows for a scenario
where anonymous users have permissions to do something while inactive
@ -247,6 +254,10 @@ authenticated users do not.
Do not forget to test for the ``is_active`` attribute of the user in your own
backend permission methods.
.. versionchanged:: 1.10
In older versions, the :class:`~django.contrib.auth.backends.ModelBackend`
allowed inactive users to authenticate.
Handling object permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,12 +1,14 @@
from .custom_permissions import CustomPermissionsUser
from .custom_user import CustomUser, ExtensionUser
from .custom_user import (
CustomUser, CustomUserWithoutIsActiveField, ExtensionUser,
)
from .invalid_models import CustomUserNonUniqueUsername
from .is_active import IsActiveTestUser1
from .uuid_pk import UUIDUser
from .with_foreign_key import CustomUserWithFK, Email
__all__ = (
'CustomUser', 'CustomPermissionsUser', 'CustomUserWithFK', 'Email',
'ExtensionUser', 'IsActiveTestUser1', 'UUIDUser',
'CustomUserNonUniqueUsername',
'CustomUser', 'CustomUserWithoutIsActiveField', 'CustomPermissionsUser',
'CustomUserWithFK', 'Email', 'ExtensionUser', 'IsActiveTestUser1',
'UUIDUser', 'CustomUserNonUniqueUsername',
)

View File

@ -97,6 +97,15 @@ class RemoveGroupsAndPermissions(object):
PermissionsMixin._meta.local_many_to_many = self._old_pm_local_m2m
class CustomUserWithoutIsActiveField(AbstractBaseUser):
username = models.CharField(max_length=150, unique=True)
email = models.EmailField(unique=True)
objects = UserManager()
USERNAME_FIELD = 'username'
# The extension user is a simple extension of the built-in user class,
# adding a required date_of_birth field. This allows us to check for
# any hard references to the name "User" in forms/handlers etc.

View File

@ -15,7 +15,10 @@ from django.test import (
SimpleTestCase, TestCase, modify_settings, override_settings,
)
from .models import CustomPermissionsUser, CustomUser, ExtensionUser, UUIDUser
from .models import (
CustomPermissionsUser, CustomUser, CustomUserWithoutIsActiveField,
ExtensionUser, UUIDUser,
)
class CountingMD5PasswordHasher(MD5PasswordHasher):
@ -200,19 +203,35 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
Tests for the ModelBackend using the default User model.
"""
UserModel = User
user_credentials = {'username': 'test', 'password': 'test'}
def create_users(self):
self.user = User.objects.create_user(
username='test',
email='test@example.com',
password='test',
)
self.user = User.objects.create_user(email='test@example.com', **self.user_credentials)
self.superuser = User.objects.create_superuser(
username='test2',
email='test2@example.com',
password='test',
)
def test_authenticate_inactive(self):
"""
An inactive user can't authenticate.
"""
self.assertEqual(authenticate(**self.user_credentials), self.user)
self.user.is_active = False
self.user.save()
self.assertIsNone(authenticate(**self.user_credentials))
@override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithoutIsActiveField')
def test_authenticate_user_without_is_active_field(self):
"""
A custom user without an `is_active` field is allowed to authenticate.
"""
user = CustomUserWithoutIsActiveField.objects._create_user(
username='test', email='test@example.com', password='test',
)
self.assertEqual(authenticate(username='test', password='test'), user)
@override_settings(AUTH_USER_MODEL='auth_tests.ExtensionUser')
class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
@ -676,3 +695,29 @@ class SelectingBackendTests(TestCase):
user = User.objects.create_user(self.username, 'email', self.password)
self.client._login(user, self.other_backend)
self.assertBackendInSession(self.other_backend)
@override_settings(AUTHENTICATION_BACKENDS=['django.contrib.auth.backends.AllowAllUsersModelBackend'])
class AllowAllUsersModelBackendTest(TestCase):
"""
Inactive users may authenticate with the AllowAllUsersModelBackend.
"""
user_credentials = {'username': 'test', 'password': 'test'}
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
email='test@example.com', is_active=False,
**cls.user_credentials
)
def test_authenticate(self):
self.assertFalse(self.user.is_active)
self.assertEqual(authenticate(**self.user_credentials), self.user)
def test_get_user(self):
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
user = get_user(request)
self.assertEqual(user, self.user)

View File

@ -166,6 +166,9 @@ class UserCreationFormTest(TestDataMixin, TestCase):
self.assertEqual(form.cleaned_data['password2'], data['password2'])
# To verify that the login form rejects inactive users, use an authentication
# backend that allows them.
@override_settings(AUTHENTICATION_BACKENDS=['django.contrib.auth.backends.AllowAllUsersModelBackend'])
class AuthenticationFormTest(TestDataMixin, TestCase):
def test_invalid_username(self):

View File

@ -145,6 +145,11 @@ class RemoteUserTest(TestCase):
# In backends that do not create new users, it is '' (anonymous user)
self.assertNotEqual(response.context['user'].username, 'knownuser')
def test_inactive_user(self):
User.objects.create(username='knownuser', is_active=False)
response = self.client.get('/remote_user/', **{self.header: 'knownuser'})
self.assertTrue(response.context['user'].is_anonymous())
class RemoteUserNoCreateBackend(RemoteUserBackend):
"""Backend that doesn't create unknown users."""
@ -166,6 +171,16 @@ class RemoteUserNoCreateTest(RemoteUserTest):
self.assertEqual(User.objects.count(), num_users)
class AllowAllUsersRemoteUserBackendTest(RemoteUserTest):
"""Backend that allows inactive users."""
backend = 'django.contrib.auth.backends.AllowAllUsersRemoteUserBackend'
def test_inactive_user(self):
user = User.objects.create(username='knownuser', is_active=False)
response = self.client.get('/remote_user/', **{self.header: self.known_user})
self.assertEqual(response.context['user'].username, user.username)
class CustomRemoteUserBackend(RemoteUserBackend):
"""
Backend that overrides RemoteUserBackend methods.

View File

@ -437,6 +437,12 @@ class ClientTest(TestCase):
login = self.client.login(username='inactive', password='password')
self.assertFalse(login)
@override_settings(
AUTHENTICATION_BACKENDS=[
'django.contrib.auth.backends.ModelBackend',
'django.contrib.auth.backends.AllowAllUsersModelBackend',
]
)
def test_view_with_inactive_force_login(self):
"Request a page that is protected with @login, but use an inactive login"
@ -445,7 +451,7 @@ class ClientTest(TestCase):
self.assertRedirects(response, '/accounts/login/?next=/login_protected_view/')
# Log in
self.client.force_login(self.u2)
self.client.force_login(self.u2, backend='django.contrib.auth.backends.AllowAllUsersModelBackend')
# Request a page that requires a login
response = self.client.get('/login_protected_view/')