1
0
mirror of https://github.com/django/django.git synced 2025-03-26 09:10:50 +00:00

Fixed #34391 -- Added async-compatible interface to auth functions and related methods test clients.

This commit is contained in:
Jon Janzen 2023-02-10 20:43:26 -05:00 committed by Mariusz Felisiak
parent 2360ba2274
commit 5e98959d92
10 changed files with 307 additions and 14 deletions

View File

@ -1,6 +1,8 @@
import inspect
import re
from asgiref.sync import sync_to_async
from django.apps import apps as django_apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
@ -91,6 +93,12 @@ def authenticate(request=None, **credentials):
)
@sensitive_variables("credentials")
async def aauthenticate(request=None, **credentials):
"""See authenticate()."""
return await sync_to_async(authenticate)(request, **credentials)
def login(request, user, backend=None):
"""
Persist a user id and a backend in the request. This way a user doesn't
@ -144,6 +152,11 @@ def login(request, user, backend=None):
user_logged_in.send(sender=user.__class__, request=request, user=user)
async def alogin(request, user, backend=None):
"""See login()."""
return await sync_to_async(login)(request, user, backend)
def logout(request):
"""
Remove the authenticated user's ID from the request and flush their session
@ -162,6 +175,11 @@ def logout(request):
request.user = AnonymousUser()
async def alogout(request):
"""See logout()."""
return await sync_to_async(logout)(request)
def get_user_model():
"""
Return the User model that is active in this project.
@ -223,6 +241,11 @@ def get_user(request):
return user or AnonymousUser()
async def aget_user(request):
"""See get_user()."""
return await sync_to_async(get_user)(request)
def get_permission_codename(action, opts):
"""
Return the codename of the permission for the specified action.
@ -242,3 +265,8 @@ def update_session_auth_hash(request, user):
request.session.cycle_key()
if hasattr(user, "get_session_auth_hash") and request.user == user:
request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()
async def aupdate_session_auth_hash(request, user):
"""See update_session_auth_hash()."""
return await sync_to_async(update_session_auth_hash)(request, user)

View File

@ -1,7 +1,5 @@
from functools import partial
from asgiref.sync import sync_to_async
from django.contrib import auth
from django.contrib.auth import load_backend
from django.contrib.auth.backends import RemoteUserBackend
@ -18,7 +16,7 @@ def get_user(request):
async def auser(request):
if not hasattr(request, "_acached_user"):
request._acached_user = await sync_to_async(auth.get_user)(request)
request._acached_user = await auth.aget_user(request)
return request._acached_user

View File

@ -747,6 +747,9 @@ class ClientMixin:
self.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
return session
async def asession(self):
return await sync_to_async(lambda: self.session)()
def login(self, **credentials):
"""
Set the Factory to appear as if it has successfully logged into a site.
@ -762,20 +765,36 @@ class ClientMixin:
return True
return False
async def alogin(self, **credentials):
"""See login()."""
from django.contrib.auth import aauthenticate
user = await aauthenticate(**credentials)
if user:
await self._alogin(user)
return True
return False
def force_login(self, user, backend=None):
def get_backend():
from django.contrib.auth import load_backend
for backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
if hasattr(backend, "get_user"):
return backend_path
if backend is None:
backend = get_backend()
backend = self._get_backend()
user.backend = backend
self._login(user, backend)
async def aforce_login(self, user, backend=None):
if backend is None:
backend = self._get_backend()
user.backend = backend
await self._alogin(user, backend)
def _get_backend(self):
from django.contrib.auth import load_backend
for backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
if hasattr(backend, "get_user"):
return backend_path
def _login(self, user, backend=None):
from django.contrib.auth import login
@ -789,6 +808,26 @@ class ClientMixin:
login(request, user, backend)
# Save the session values.
request.session.save()
self._set_login_cookies(request)
async def _alogin(self, user, backend=None):
from django.contrib.auth import alogin
# Create a fake request to store login details.
request = HttpRequest()
session = await self.asession()
if session:
request.session = session
else:
engine = import_module(settings.SESSION_ENGINE)
request.session = engine.SessionStore()
await alogin(request, user, backend)
# Save the session values.
await sync_to_async(request.session.save)()
self._set_login_cookies(request)
def _set_login_cookies(self, request):
# Set the cookie to represent the session.
session_cookie = settings.SESSION_COOKIE_NAME
self.cookies[session_cookie] = request.session.session_key
@ -815,6 +854,21 @@ class ClientMixin:
logout(request)
self.cookies = SimpleCookie()
async def alogout(self):
"""See logout()."""
from django.contrib.auth import aget_user, alogout
request = HttpRequest()
session = await self.asession()
if session:
request.session = session
request.user = await aget_user(request)
else:
engine = import_module(settings.SESSION_ENGINE)
request.session = engine.SessionStore()
await alogout(request)
self.cookies = SimpleCookie()
def _parse_json(self, response, **extra):
if not hasattr(response, "_json"):
if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")):

View File

@ -693,6 +693,9 @@ Utility functions
.. currentmodule:: django.contrib.auth
.. function:: get_user(request)
.. function:: aget_user(request)
*Asynchronous version*: ``aget_user()``
Returns the user model instance associated with the given ``request``s
session.
@ -716,3 +719,7 @@ Utility functions
.. versionchanged:: 4.1.8
Fallback verification with :setting:`SECRET_KEY_FALLBACKS` was added.
.. versionchanged:: 5.0
``aget_user()`` function was added.

View File

@ -150,6 +150,12 @@ Minor features
* The default iteration count for the PBKDF2 password hasher is increased from
600,000 to 720,000.
* The new asynchronous functions are now provided, using an
``a`` prefix: :func:`django.contrib.auth.aauthenticate`,
:func:`~.django.contrib.auth.aget_user`,
:func:`~.django.contrib.auth.alogin`, :func:`~.django.contrib.auth.alogout`,
and :func:`~.django.contrib.auth.aupdate_session_auth_hash`.
* ``AuthenticationMiddleware`` now adds an :meth:`.HttpRequest.auser`
asynchronous method that returns the currently logged-in user.
@ -366,7 +372,11 @@ Templates
Tests
~~~~~
* ...
* :class:`~django.test.Client` and :class:`~django.test.AsyncClient` now
provide asynchronous methods, using an ``a`` prefix:
:meth:`~django.test.Client.asession`, :meth:`~django.test.Client.alogin`,
:meth:`~django.test.Client.aforce_login`, and
:meth:`~django.test.Client.alogout`.
URLs
~~~~

View File

@ -120,6 +120,9 @@ Authenticating users
--------------------
.. function:: authenticate(request=None, **credentials)
.. function:: aauthenticate(request=None, **credentials)
*Asynchronous version*: ``aauthenticate()``
Use :func:`~django.contrib.auth.authenticate()` to verify a set of
credentials. It takes credentials as keyword arguments, ``username`` and
@ -152,6 +155,10 @@ Authenticating users
this. Rather if you're looking for a way to login a user, use the
:class:`~django.contrib.auth.views.LoginView`.
.. versionchanged:: 5.0
``aauthenticate()`` function was added.
.. _topic-authorization:
Permissions and Authorization
@ -407,6 +414,9 @@ If you have an authenticated user you want to attach to the current session
- this is done with a :func:`~django.contrib.auth.login` function.
.. function:: login(request, user, backend=None)
.. function:: alogin(request, user, backend=None)
*Asynchronous version*: ``alogin()``
To log a user in, from a view, use :func:`~django.contrib.auth.login()`. It
takes an :class:`~django.http.HttpRequest` object and a
@ -436,6 +446,10 @@ If you have an authenticated user you want to attach to the current session
# Return an 'invalid login' error message.
...
.. versionchanged:: 5.0
``alogin()`` function was added.
Selecting the authentication backend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -463,6 +477,9 @@ How to log a user out
---------------------
.. function:: logout(request)
.. function:: alogout(request)
*Asynchronous version*: ``alogout()``
To log out a user who has been logged in via
:func:`django.contrib.auth.login()`, use
@ -488,6 +505,10 @@ How to log a user out
immediately after logging out, do that *after* calling
:func:`django.contrib.auth.logout()`.
.. versionchanged:: 5.0
``alogout()`` function was added.
Limiting access to logged-in users
----------------------------------
@ -935,6 +956,9 @@ and wish to have similar behavior, use the :func:`update_session_auth_hash`
function.
.. function:: update_session_auth_hash(request, user)
.. function:: aupdate_session_auth_hash(request, user)
*Asynchronous version*: ``aupdate_session_auth_hash()``
This function takes the current request and the updated user object from
which the new session hash will be derived and updates the session hash
@ -955,6 +979,10 @@ function.
else:
...
.. versionchanged:: 5.0
``aupdate_session_auth_hash()`` function was added.
.. note::
Since

View File

@ -440,6 +440,9 @@ Use the ``django.test.Client`` class to make requests.
The ``headers`` parameter was added.
.. method:: Client.login(**credentials)
.. method:: Client.alogin(**credentials)
*Asynchronous version*: ``alogin()``
If your site uses Django's :doc:`authentication system</topics/auth/index>`
and you deal with logging in users, you can use the test client's
@ -485,7 +488,14 @@ Use the ``django.test.Client`` class to make requests.
:meth:`~django.contrib.auth.models.UserManager.create_user` helper
method to create a new user with a correctly hashed password.
.. versionchanged:: 5.0
``alogin()`` method was added.
.. method:: Client.force_login(user, backend=None)
.. method:: Client.aforce_login(user, backend=None)
*Asynchronous version*: ``aforce_login()``
If your site uses Django's :doc:`authentication
system</topics/auth/index>`, you can use the ``force_login()`` method
@ -509,7 +519,14 @@ Use the ``django.test.Client`` class to make requests.
``login()`` by :ref:`using a weaker hasher while testing
<speeding-up-tests-auth-hashers>`.
.. versionchanged:: 5.0
``aforce_login()`` method was added.
.. method:: Client.logout()
.. method:: Client.alogout()
*Asynchronous version*: ``alogout()``
If your site uses Django's :doc:`authentication system</topics/auth/index>`,
the ``logout()`` method can be used to simulate the effect of a user
@ -519,6 +536,10 @@ Use the ``django.test.Client`` class to make requests.
and session data cleared to defaults. Subsequent requests will appear
to come from an :class:`~django.contrib.auth.models.AnonymousUser`.
.. versionchanged:: 5.0
``alogout()`` method was added.
Testing responses
-----------------
@ -703,6 +724,13 @@ access these properties as part of a test condition.
session["somekey"] = "test"
session.save()
.. method:: Client.asession()
.. versionadded:: 5.0
This is similar to the :attr:`session` attribute but it works in async
contexts.
Setting the language
--------------------

View File

@ -0,0 +1,98 @@
from django.contrib.auth import (
aauthenticate,
aget_user,
alogin,
alogout,
aupdate_session_auth_hash,
)
from django.contrib.auth.models import AnonymousUser, User
from django.http import HttpRequest
from django.test import TestCase, override_settings
class AsyncAuthTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.test_user = User.objects.create_user(
"testuser", "test@example.com", "testpw"
)
async def test_aauthenticate(self):
user = await aauthenticate(username="testuser", password="testpw")
self.assertIsInstance(user, User)
self.assertEqual(user.username, self.test_user.username)
user.is_active = False
await user.asave()
self.assertIsNone(await aauthenticate(username="testuser", password="testpw"))
async def test_alogin(self):
request = HttpRequest()
request.session = await self.client.asession()
await alogin(request, self.test_user)
user = await aget_user(request)
self.assertIsInstance(user, User)
self.assertEqual(user.username, self.test_user.username)
async def test_alogin_without_user(self):
request = HttpRequest()
request.user = self.test_user
request.session = await self.client.asession()
await alogin(request, None)
user = await aget_user(request)
self.assertIsInstance(user, User)
self.assertEqual(user.username, self.test_user.username)
async def test_alogout(self):
await self.client.alogin(username="testuser", password="testpw")
request = HttpRequest()
request.session = await self.client.asession()
await alogout(request)
user = await aget_user(request)
self.assertIsInstance(user, AnonymousUser)
async def test_client_alogout(self):
await self.client.alogin(username="testuser", password="testpw")
request = HttpRequest()
request.session = await self.client.asession()
await self.client.alogout()
user = await aget_user(request)
self.assertIsInstance(user, AnonymousUser)
async def test_change_password(self):
await self.client.alogin(username="testuser", password="testpw")
request = HttpRequest()
request.session = await self.client.asession()
request.user = self.test_user
await aupdate_session_auth_hash(request, self.test_user)
user = await aget_user(request)
self.assertIsInstance(user, User)
async def test_invalid_login(self):
self.assertEqual(
await self.client.alogin(username="testuser", password=""), False
)
async def test_client_aforce_login(self):
await self.client.aforce_login(self.test_user)
request = HttpRequest()
request.session = await self.client.asession()
user = await aget_user(request)
self.assertEqual(user.username, self.test_user.username)
@override_settings(
AUTHENTICATION_BACKENDS=[
"django.contrib.auth.backends.ModelBackend",
"django.contrib.auth.backends.AllowAllUsersModelBackend",
]
)
async def test_client_aforce_login_backend(self):
self.test_user.is_active = False
await self.test_user.asave()
await self.client.aforce_login(
self.test_user,
backend="django.contrib.auth.backends.AllowAllUsersModelBackend",
)
request = HttpRequest()
request.session = await self.client.asession()
user = await aget_user(request)
self.assertEqual(user.username, self.test_user.username)

View File

@ -6,6 +6,7 @@ from django.contrib.auth import (
BACKEND_SESSION_KEY,
SESSION_KEY,
_clean_credentials,
aauthenticate,
authenticate,
get_user,
signals,
@ -764,6 +765,28 @@ class AuthenticateTests(TestCase):
status_code=500,
)
@override_settings(
AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.TypeErrorBackend"]
)
async def test_aauthenticate_sensitive_variables(self):
try:
await aauthenticate(
username="testusername", password=self.sensitive_password
)
except TypeError:
exc_info = sys.exc_info()
rf = RequestFactory()
response = technical_500_response(rf.get("/"), *exc_info)
self.assertNotContains(response, self.sensitive_password, status_code=500)
self.assertContains(response, "TypeErrorBackend", status_code=500)
self.assertContains(
response,
'<tr><td>credentials</td><td class="code">'
"<pre>&#39;********************&#39;</pre></td></tr>",
html=True,
status_code=500,
)
def test_clean_credentials_sensitive_variables(self):
try:
# Passing in a list to cause an exception

View File

@ -1,5 +1,7 @@
from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib.auth import get_user, get_user_model
from django.contrib.auth import aget_user, get_user, get_user_model
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError
@ -129,6 +131,12 @@ class TestGetUser(TestCase):
user = get_user(request)
self.assertIsInstance(user, AnonymousUser)
async def test_aget_user_anonymous(self):
request = HttpRequest()
request.session = await self.client.asession()
user = await aget_user(request)
self.assertIsInstance(user, AnonymousUser)
def test_get_user(self):
created_user = User.objects.create_user(
"testuser", "test@example.com", "testpw"
@ -162,3 +170,14 @@ class TestGetUser(TestCase):
user = get_user(request)
self.assertIsInstance(user, User)
self.assertEqual(user.username, created_user.username)
async def test_aget_user(self):
created_user = await sync_to_async(User.objects.create_user)(
"testuser", "test@example.com", "testpw"
)
await self.client.alogin(username="testuser", password="testpw")
request = HttpRequest()
request.session = await self.client.asession()
user = await aget_user(request)
self.assertIsInstance(user, User)
self.assertEqual(user.username, created_user.username)