From 00f5d2d110712af84fae2c5f9183a2ea48ce0a4a Mon Sep 17 00:00:00 2001 From: Ben Lomax Date: Wed, 26 Apr 2023 07:08:33 +0100 Subject: [PATCH] Refs #31949 -- Made @xframe_options_(deny/sameorigin/exempt) decorators to work with async functions. --- AUTHORS | 1 + django/views/decorators/clickjacking.py | 68 +++++++++++++++------- docs/ref/clickjacking.txt | 10 ++++ docs/releases/5.0.txt | 10 +++- docs/topics/async.txt | 3 + tests/decorators/test_clickjacking.py | 75 +++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 23 deletions(-) diff --git a/AUTHORS b/AUTHORS index d4d2c47d93..2e3a91c756 100644 --- a/AUTHORS +++ b/AUTHORS @@ -137,6 +137,7 @@ answer newbie questions, and generally made Django that much better: Ben Godfrey Benjamin Wohlwend Ben Khoo + Ben Lomax Ben Slavin Ben Sturmfels Berker Peksag diff --git a/django/views/decorators/clickjacking.py b/django/views/decorators/clickjacking.py index 8fa49ddb80..c20fa59d2a 100644 --- a/django/views/decorators/clickjacking.py +++ b/django/views/decorators/clickjacking.py @@ -1,5 +1,7 @@ from functools import wraps +from asgiref.sync import iscoroutinefunction + def xframe_options_deny(view_func): """ @@ -12,14 +14,23 @@ def xframe_options_deny(view_func): ... """ - @wraps(view_func) - def wrapper_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - if resp.get("X-Frame-Options") is None: - resp["X-Frame-Options"] = "DENY" - return resp + if iscoroutinefunction(view_func): - return wrapper_view + async def _view_wrapper(*args, **kwargs): + response = await view_func(*args, **kwargs) + if response.get("X-Frame-Options") is None: + response["X-Frame-Options"] = "DENY" + return response + + else: + + def _view_wrapper(*args, **kwargs): + response = view_func(*args, **kwargs) + if response.get("X-Frame-Options") is None: + response["X-Frame-Options"] = "DENY" + return response + + return wraps(view_func)(_view_wrapper) def xframe_options_sameorigin(view_func): @@ -33,14 +44,23 @@ def xframe_options_sameorigin(view_func): ... """ - @wraps(view_func) - def wrapper_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - if resp.get("X-Frame-Options") is None: - resp["X-Frame-Options"] = "SAMEORIGIN" - return resp + if iscoroutinefunction(view_func): - return wrapper_view + async def _view_wrapper(*args, **kwargs): + response = await view_func(*args, **kwargs) + if response.get("X-Frame-Options") is None: + response["X-Frame-Options"] = "SAMEORIGIN" + return response + + else: + + def _view_wrapper(*args, **kwargs): + response = view_func(*args, **kwargs) + if response.get("X-Frame-Options") is None: + response["X-Frame-Options"] = "SAMEORIGIN" + return response + + return wraps(view_func)(_view_wrapper) def xframe_options_exempt(view_func): @@ -53,10 +73,18 @@ def xframe_options_exempt(view_func): ... """ - @wraps(view_func) - def wrapper_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - resp.xframe_options_exempt = True - return resp + if iscoroutinefunction(view_func): - return wrapper_view + async def _view_wrapper(*args, **kwargs): + response = await view_func(*args, **kwargs) + response.xframe_options_exempt = True + return response + + else: + + def _view_wrapper(*args, **kwargs): + response = view_func(*args, **kwargs) + response.xframe_options_exempt = True + return response + + return wraps(view_func)(_view_wrapper) diff --git a/docs/ref/clickjacking.txt b/docs/ref/clickjacking.txt index f9bec591a7..3a81bdbdb0 100644 --- a/docs/ref/clickjacking.txt +++ b/docs/ref/clickjacking.txt @@ -90,6 +90,11 @@ that tells the middleware not to set the header:: iframe, you may need to modify the :setting:`CSRF_COOKIE_SAMESITE` or :setting:`SESSION_COOKIE_SAMESITE` settings. +.. versionchanged:: 5.0 + + Support for wrapping asynchronous view functions was added to the + ``@xframe_options_exempt`` decorator. + Setting ``X-Frame-Options`` per view ------------------------------------ @@ -113,6 +118,11 @@ decorators:: Note that you can use the decorators in conjunction with the middleware. Use of a decorator overrides the middleware. +.. versionchanged:: 5.0 + + Support for wrapping asynchronous view functions was added to the + ``@xframe_options_deny`` and ``@xframe_options_sameorigin`` decorators. + Limitations =========== diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index 0c093720b9..611a7bd68b 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -233,9 +233,13 @@ CSRF Decorators ~~~~~~~~~~ -* The :func:`~django.views.decorators.cache.cache_control` and - :func:`~django.views.decorators.cache.never_cache` decorators now support - wrapping asynchronous view functions. +* The following decorators now support wrapping asynchronous view functions: + + * :func:`~django.views.decorators.cache.cache_control` + * :func:`~django.views.decorators.cache.never_cache` + * ``xframe_options_deny()`` + * ``xframe_options_sameorigin()`` + * ``xframe_options_exempt()`` Email ~~~~~ diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 769fac3c52..444c88a1c6 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -83,6 +83,9 @@ view functions: * :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.never_cache` +* ``xframe_options_deny()`` +* ``xframe_options_sameorigin()`` +* ``xframe_options_exempt()`` For example:: diff --git a/tests/decorators/test_clickjacking.py b/tests/decorators/test_clickjacking.py index 278950082d..fe4c551648 100644 --- a/tests/decorators/test_clickjacking.py +++ b/tests/decorators/test_clickjacking.py @@ -1,3 +1,5 @@ +from asgiref.sync import iscoroutinefunction + from django.http import HttpRequest, HttpResponse from django.middleware.clickjacking import XFrameOptionsMiddleware from django.test import SimpleTestCase @@ -9,6 +11,20 @@ from django.views.decorators.clickjacking import ( class XFrameOptionsDenyTests(SimpleTestCase): + def test_wrapped_sync_function_is_not_coroutine_function(self): + def sync_view(request): + return HttpResponse() + + wrapped_view = xframe_options_deny(sync_view) + self.assertIs(iscoroutinefunction(wrapped_view), False) + + def test_wrapped_async_function_is_coroutine_function(self): + async def async_view(request): + return HttpResponse() + + wrapped_view = xframe_options_deny(async_view) + self.assertIs(iscoroutinefunction(wrapped_view), True) + def test_decorator_sets_x_frame_options_to_deny(self): @xframe_options_deny def a_view(request): @@ -17,8 +33,30 @@ class XFrameOptionsDenyTests(SimpleTestCase): response = a_view(HttpRequest()) self.assertEqual(response.headers["X-Frame-Options"], "DENY") + async def test_decorator_sets_x_frame_options_to_deny_async_view(self): + @xframe_options_deny + async def an_async_view(request): + return HttpResponse() + + response = await an_async_view(HttpRequest()) + self.assertEqual(response.headers["X-Frame-Options"], "DENY") + class XFrameOptionsSameoriginTests(SimpleTestCase): + def test_wrapped_sync_function_is_not_coroutine_function(self): + def sync_view(request): + return HttpResponse() + + wrapped_view = xframe_options_sameorigin(sync_view) + self.assertIs(iscoroutinefunction(wrapped_view), False) + + def test_wrapped_async_function_is_coroutine_function(self): + async def async_view(request): + return HttpResponse() + + wrapped_view = xframe_options_sameorigin(async_view) + self.assertIs(iscoroutinefunction(wrapped_view), True) + def test_decorator_sets_x_frame_options_to_sameorigin(self): @xframe_options_sameorigin def a_view(request): @@ -27,8 +65,30 @@ class XFrameOptionsSameoriginTests(SimpleTestCase): response = a_view(HttpRequest()) self.assertEqual(response.headers["X-Frame-Options"], "SAMEORIGIN") + async def test_decorator_sets_x_frame_options_to_sameorigin_async_view(self): + @xframe_options_sameorigin + async def an_async_view(request): + return HttpResponse() + + response = await an_async_view(HttpRequest()) + self.assertEqual(response.headers["X-Frame-Options"], "SAMEORIGIN") + class XFrameOptionsExemptTests(SimpleTestCase): + def test_wrapped_sync_function_is_not_coroutine_function(self): + def sync_view(request): + return HttpResponse() + + wrapped_view = xframe_options_exempt(sync_view) + self.assertIs(iscoroutinefunction(wrapped_view), False) + + def test_wrapped_async_function_is_coroutine_function(self): + async def async_view(request): + return HttpResponse() + + wrapped_view = xframe_options_exempt(async_view) + self.assertIs(iscoroutinefunction(wrapped_view), True) + def test_decorator_stops_x_frame_options_being_set(self): """ @xframe_options_exempt instructs the XFrameOptionsMiddleware to NOT set @@ -48,3 +108,18 @@ class XFrameOptionsExemptTests(SimpleTestCase): # middleware's functionality. middleware_response = XFrameOptionsMiddleware(a_view)(request) self.assertIsNone(middleware_response.get("X-Frame-Options")) + + async def test_exempt_decorator_async_view(self): + @xframe_options_exempt + async def an_async_view(request): + return HttpResponse() + + request = HttpRequest() + response = await an_async_view(request) + self.assertIsNone(response.get("X-Frame-Options")) + self.assertIs(response.xframe_options_exempt, True) + + # The real purpose of the exempt decorator is to suppress the + # middleware's functionality. + middleware_response = await XFrameOptionsMiddleware(an_async_view)(request) + self.assertIsNone(middleware_response.get("X-Frame-Options"))