From 53f5dc10cdc0c1fbeb48b2d98fbdd26c1996ec59 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 24 Feb 2017 10:15:41 -0500 Subject: [PATCH] [1.11.x] Refs #25187 -- Fixed AuthBackend.authenticate() compatibility for signatures that accept a request kwarg. Backport of c31e7ab5a4b062225bc4f6b5cae065325dd30f1f from master --- django/contrib/auth/__init__.py | 26 ++++++++++++++----- docs/internals/deprecation.txt | 4 +-- docs/releases/1.11.txt | 3 ++- .../test_auth_backends_deprecation.py | 24 ++++++++++++++++- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 04a7f7ddcb..426bd6982b 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -67,21 +67,35 @@ def authenticate(request=None, **credentials): """ for backend, backend_path in _get_backends(return_tuples=True): args = (request,) + # Does the backend accept a request argument? try: inspect.getcallargs(backend.authenticate, request, **credentials) except TypeError: + args = () + # Does the backend accept a request keyword argument? try: - inspect.getcallargs(backend.authenticate, **credentials) + inspect.getcallargs(backend.authenticate, request=request, **credentials) except TypeError: - # This backend doesn't accept these credentials as arguments. Try the next one. - continue + # Does the backend accept credentials without request? + try: + inspect.getcallargs(backend.authenticate, **credentials) + except TypeError: + # This backend doesn't accept these credentials as arguments. Try the next one. + continue + else: + warnings.warn( + "Update %s.authenticate() to accept a positional " + "`request` argument." % backend_path, + RemovedInDjango21Warning + ) else: - args = () + credentials['request'] = request warnings.warn( - "Update authentication backend %s to accept a " - "positional `request` argument." % backend_path, + "In %s.authenticate(), move the `request` keyword argument " + "to the first positional argument." % backend_path, RemovedInDjango21Warning ) + try: user = backend.authenticate(*args, **credentials) except PermissionDenied: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 42db85127f..f679ad9256 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -38,8 +38,8 @@ details on these changes. * ``DatabaseIntrospection.get_indexes()`` will be removed. -* The ``authenticate()`` method of authentication backends will require a - ``request`` argument. +* The ``authenticate()`` method of authentication backends will require + ``request`` as the first positional argument. * The ``django.db.models.permalink()`` decorator will be removed. diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 3fb4968e23..eb983de0dc 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -796,7 +796,8 @@ Miscellaneous * :func:`~django.contrib.auth.authenticate` now passes a ``request`` argument to the ``authenticate()`` method of authentication backends. Support for - methods that don't accept ``request`` will be removed in Django 2.1. + methods that don't accept ``request`` as the first positional argument will + be removed in Django 2.1. * The ``USE_ETAGS`` setting is deprecated in favor of :class:`~django.middleware.http.ConditionalGetMiddleware` which now adds the diff --git a/tests/auth_tests/test_auth_backends_deprecation.py b/tests/auth_tests/test_auth_backends_deprecation.py index 6178936535..0d45fee7a4 100644 --- a/tests/auth_tests/test_auth_backends_deprecation.py +++ b/tests/auth_tests/test_auth_backends_deprecation.py @@ -3,6 +3,8 @@ import warnings from django.contrib.auth import authenticate from django.test import SimpleTestCase, override_settings +mock_request = object() + class NoRequestBackend(object): def authenticate(self, username=None, password=None): @@ -10,12 +12,20 @@ class NoRequestBackend(object): pass +class RequestNotPositionArgBackend: + def authenticate(self, username=None, password=None, request=None): + assert username == 'username' + assert password == 'pass' + assert request is mock_request + + class AcceptsRequestBackendTest(SimpleTestCase): """ A deprecation warning is shown for backends that have an authenticate() method without a request parameter. """ no_request_backend = '%s.NoRequestBackend' % __name__ + request_not_positional_backend = '%s.RequestNotPositionArgBackend' % __name__ @override_settings(AUTHENTICATION_BACKENDS=[no_request_backend]) def test_no_request_deprecation_warning(self): @@ -25,6 +35,18 @@ class AcceptsRequestBackendTest(SimpleTestCase): self.assertEqual(len(warns), 1) self.assertEqual( str(warns[0].message), - "Update authentication backend %s to accept a positional `request` " + "Update %s.authenticate() to accept a positional `request` " "argument." % self.no_request_backend ) + + @override_settings(AUTHENTICATION_BACKENDS=[request_not_positional_backend]) + def test_request_keyword_arg_deprecation_warning(self): + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + authenticate(username='username', password='pass', request=mock_request) + self.assertEqual(len(warns), 1) + self.assertEqual( + str(warns[0].message), + "In %s.authenticate(), move the `request` keyword argument to the " + "first positional argument." % self.request_not_positional_backend + )