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:
parent
2360ba2274
commit
5e98959d92
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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")):
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
~~~~
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
--------------------
|
||||
|
||||
|
98
tests/async/test_async_auth.py
Normal file
98
tests/async/test_async_auth.py
Normal 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)
|
@ -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>'********************'</pre></td></tr>",
|
||||
html=True,
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
def test_clean_credentials_sensitive_variables(self):
|
||||
try:
|
||||
# Passing in a list to cause an exception
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user