Fixed #33561 -- Allowed synchronization of user attributes in RemoteUserBackend.

This commit is contained in:
Adrian Torres 2022-03-04 11:04:07 +01:00 committed by Mariusz Felisiak
parent 67b5f506a6
commit d90e34c61b
6 changed files with 93 additions and 13 deletions

View File

@ -23,6 +23,7 @@ answer newbie questions, and generally made Django that much better:
Adiyat Mubarak <adiyatmubarak@gmail.com> Adiyat Mubarak <adiyatmubarak@gmail.com>
Adnan Umer <u.adnan@outlook.com> Adnan Umer <u.adnan@outlook.com>
Adrian Holovaty <adrian@holovaty.com> Adrian Holovaty <adrian@holovaty.com>
Adrian Torres <atorresj@redhat.com>
Adrien Lemaire <lemaire.adrien@gmail.com> Adrien Lemaire <lemaire.adrien@gmail.com>
Afonso Fernández Nogueira <fonzzo.django@gmail.com> Afonso Fernández Nogueira <fonzzo.django@gmail.com>
AgarFu <heaven@croasanaso.sytes.net> AgarFu <heaven@croasanaso.sytes.net>

View File

@ -1,6 +1,10 @@
import warnings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.inspect import func_supports_parameter
UserModel = get_user_model() UserModel = get_user_model()
@ -192,6 +196,7 @@ class RemoteUserBackend(ModelBackend):
""" """
if not remote_user: if not remote_user:
return return
created = False
user = None user = None
username = self.clean_username(remote_user) username = self.clean_username(remote_user)
@ -202,13 +207,24 @@ class RemoteUserBackend(ModelBackend):
user, created = UserModel._default_manager.get_or_create( user, created = UserModel._default_manager.get_or_create(
**{UserModel.USERNAME_FIELD: username} **{UserModel.USERNAME_FIELD: username}
) )
if created:
user = self.configure_user(request, user)
else: else:
try: try:
user = UserModel._default_manager.get_by_natural_key(username) user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist: except UserModel.DoesNotExist:
pass pass
# RemovedInDjango50Warning: When the deprecation ends, replace with:
# user = self.configure_user(request, user, created=created)
if func_supports_parameter(self.configure_user, "created"):
user = self.configure_user(request, user, created=created)
else:
warnings.warn(
f"`created=True` must be added to the signature of "
f"{self.__class__.__qualname__}.configure_user().",
category=RemovedInDjango50Warning,
)
if created:
user = self.configure_user(request, user)
return user if self.user_can_authenticate(user) else None return user if self.user_can_authenticate(user) else None
def clean_username(self, username): def clean_username(self, username):
@ -220,9 +236,9 @@ class RemoteUserBackend(ModelBackend):
""" """
return username return username
def configure_user(self, request, user): def configure_user(self, request, user, created=True):
""" """
Configure a user after creation and return the updated user. Configure a user and return the updated user.
By default, return the user unmodified. By default, return the user unmodified.
""" """

View File

@ -87,6 +87,9 @@ details on these changes.
* Passing unsaved model instances to related filters will no longer be allowed. * Passing unsaved model instances to related filters will no longer be allowed.
* ``created=True`` will be required in the signature of
``RemoteUserBackend.configure_user()`` subclasses.
.. _deprecation-removed-in-4.1: .. _deprecation-removed-in-4.1:
4.1 4.1

View File

@ -649,17 +649,27 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
information) prior to using it to get or create a user object. Returns information) prior to using it to get or create a user object. Returns
the cleaned username. the cleaned username.
.. method:: configure_user(request, user) .. method:: configure_user(request, user, created=True)
Configures a newly created user. This method is called immediately Configures the user on each authentication attempt. This method is
after a new user is created, and can be used to perform custom setup called immediately after fetching or creating the user being
actions, such as setting the user's groups based on attributes in an authenticated, and can be used to perform custom setup actions, such as
LDAP directory. Returns the user object. setting the user's groups based on attributes in an LDAP directory.
Returns the user object.
The setup can be performed either once when the user is created
(``created`` is ``True``) or on existing users (``created`` is
``False``) as a way of synchronizing attributes between the remote and
the local systems.
``request`` is an :class:`~django.http.HttpRequest` and may be ``None`` ``request`` is an :class:`~django.http.HttpRequest` and may be ``None``
if it wasn't provided to :func:`~django.contrib.auth.authenticate` if it wasn't provided to :func:`~django.contrib.auth.authenticate`
(which passes it on to the backend). (which passes it on to the backend).
.. versionchanged:: 4.1
The ``created`` argument was added.
.. method:: user_can_authenticate() .. method:: user_can_authenticate()
Returns whether the user is allowed to authenticate. This method Returns whether the user is allowed to authenticate. This method

View File

@ -74,6 +74,9 @@ Minor features
* The default iteration count for the PBKDF2 password hasher is increased from * The default iteration count for the PBKDF2 password hasher is increased from
320,000 to 390,000. 320,000 to 390,000.
* The :meth:`.RemoteUserBackend.configure_user` method now allows synchronizing
user attributes with attributes in a remote system such as an LDAP directory.
:mod:`django.contrib.contenttypes` :mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -493,6 +496,10 @@ Miscellaneous
* Passing unsaved model instances to related filters is deprecated. In Django * Passing unsaved model instances to related filters is deprecated. In Django
5.0, the exception will be raised. 5.0, the exception will be raised.
* ``created=True`` is added to the signature of
:meth:`.RemoteUserBackend.configure_user`. Support for ``RemoteUserBackend``
subclasses that do not accept this argument is deprecated.
Features removed in 4.1 Features removed in 4.1
======================= =======================

View File

@ -6,8 +6,15 @@ from django.contrib.auth.backends import RemoteUserBackend
from django.contrib.auth.middleware import RemoteUserMiddleware from django.contrib.auth.middleware import RemoteUserMiddleware
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.middleware.csrf import _get_new_csrf_string, _mask_cipher_secret from django.middleware.csrf import _get_new_csrf_string, _mask_cipher_secret
from django.test import Client, TestCase, modify_settings, override_settings from django.test import (
Client,
TestCase,
ignore_warnings,
modify_settings,
override_settings,
)
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
@override_settings(ROOT_URLCONF="auth_tests.urls") @override_settings(ROOT_URLCONF="auth_tests.urls")
@ -215,11 +222,14 @@ class CustomRemoteUserBackend(RemoteUserBackend):
""" """
return username.split("@")[0] return username.split("@")[0]
def configure_user(self, request, user): def configure_user(self, request, user, created=True):
""" """
Sets user's email address using the email specified in an HTTP header. Sets user's email address using the email specified in an HTTP header.
Sets user's last name for existing users.
""" """
user.email = request.META.get(RemoteUserTest.email_header, "") user.email = request.META.get(RemoteUserTest.email_header, "")
if not created:
user.last_name = user.username
user.save() user.save()
return user return user
@ -242,8 +252,12 @@ class RemoteUserCustomTest(RemoteUserTest):
should not have been configured with an email address. should not have been configured with an email address.
""" """
super().test_known_user() super().test_known_user()
self.assertEqual(User.objects.get(username="knownuser").email, "") knownuser = User.objects.get(username="knownuser")
self.assertEqual(User.objects.get(username="knownuser2").email, "") knownuser2 = User.objects.get(username="knownuser2")
self.assertEqual(knownuser.email, "")
self.assertEqual(knownuser2.email, "")
self.assertEqual(knownuser.last_name, "knownuser")
self.assertEqual(knownuser2.last_name, "knownuser2")
def test_unknown_user(self): def test_unknown_user(self):
""" """
@ -260,11 +274,40 @@ class RemoteUserCustomTest(RemoteUserTest):
) )
self.assertEqual(response.context["user"].username, "newuser") self.assertEqual(response.context["user"].username, "newuser")
self.assertEqual(response.context["user"].email, "user@example.com") self.assertEqual(response.context["user"].email, "user@example.com")
self.assertEqual(response.context["user"].last_name, "")
self.assertEqual(User.objects.count(), num_users + 1) self.assertEqual(User.objects.count(), num_users + 1)
newuser = User.objects.get(username="newuser") newuser = User.objects.get(username="newuser")
self.assertEqual(newuser.email, "user@example.com") self.assertEqual(newuser.email, "user@example.com")
# RemovedInDjango50Warning.
class CustomRemoteUserNoCreatedArgumentBackend(CustomRemoteUserBackend):
def configure_user(self, request, user):
return super().configure_user(request, user)
@ignore_warnings(category=RemovedInDjango50Warning)
class RemoteUserCustomNoCreatedArgumentTest(RemoteUserTest):
backend = "auth_tests.test_remote_user.CustomRemoteUserNoCreatedArgumentBackend"
@override_settings(ROOT_URLCONF="auth_tests.urls")
@modify_settings(
AUTHENTICATION_BACKENDS={
"append": "auth_tests.test_remote_user.CustomRemoteUserNoCreatedArgumentBackend"
},
MIDDLEWARE={"append": "django.contrib.auth.middleware.RemoteUserMiddleware"},
)
class RemoteUserCustomNoCreatedArgumentDeprecationTest(TestCase):
def test_known_user_sync(self):
msg = (
"`created=True` must be added to the signature of "
"CustomRemoteUserNoCreatedArgumentBackend.configure_user()."
)
with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
self.client.get("/remote_user/", **{RemoteUserTest.header: "newuser"})
class CustomHeaderMiddleware(RemoteUserMiddleware): class CustomHeaderMiddleware(RemoteUserMiddleware):
""" """
Middleware that overrides custom HTTP auth user header. Middleware that overrides custom HTTP auth user header.