8000 Refs #31949 -- Made @sensitive_variables/sensitive_post_parameters de… · django/django@38e391e · GitHub
[go: up one dir, main page]

Skip to content

Commit 38e391e

Browse files
bigfootjonfelixxm
andcommitted
Refs #31949 -- Made @sensitive_variables/sensitive_post_parameters decorators to work with async functions.
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
1 parent f8092ee commit 38e391e

File tree

6 files changed

+302
-37
lines changed

6 files changed

+302
-37
lines changed

django/views/debug.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import inspect
23
import itertools
34
import re
45
import sys
@@ -17,6 +18,7 @@
1718
from django.utils.module_loading import import_string
1819
from django.utils.regex_helper import _lazy_re_compile
1920
from django.utils.version import PY311, get_docs_version
21+
from django.views.decorators.debug import coroutine_functions_to_sensitive_variables
2022

2123
# Minimal Django templates engine to render the error templates
2224
# regardless of the project's TEMPLATES setting. Templates are
@@ -239,21 +241,37 @@ def get_traceback_frame_variables(self, request, tb_frame):
239241
Replace the values of variables marked as sensitive with
240242
stars (*********).
241243
"""
242-
# Loop through the frame's callers to see if the sensitive_variables
243-
# decorator was used.
244-
current_frame = tb_frame.f_back
245244
sensitive_variables = None
246-
while current_frame is not None:
247-
if (
248-
current_frame.f_code.co_name == "sensitive_variables_wrapper"
249-
and "sensitive_variables_wrapper" in current_frame.f_locals
250-
):
251-
# The sensitive_variables decorator was used, so we take note
252-
# of the sensitive variables' names.
253-
wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
254-
sensitive_variables = getattr(wrapper, "sensitive_variables", None)
255-
break
256-
current_frame = current_frame.f_back
245+
246+
# Coroutines don't have a proper `f_back` so they need to be inspected
247+
# separately. Handle this by stashing the registered sensitive
248+
# variables in a global dict indexed by `hash(file_path:line_number)`.
249+
if (
250+
tb_frame.f_code.co_flags & inspect.CO_COROUTINE != 0
251+
and tb_frame.f_code.co_name != "sensitive_variables_wrapper"
252+
):
253+
key = hash(
254+
f"{tb_frame.f_code.co_filename}:{tb_frame.f_code.co_firstlineno}"
255+
)
256+
sensitive_variables = coroutine_functions_to_sensitive_variables.get(
257+
key, None
258+
)
259+
260+
if sensitive_variables is None:
261+
# Loop through the frame's callers to see if the
262+
# sensitive_variables decorator was used.
263+
current_frame = tb_frame
264+
while current_frame is not None:
265+
if (
266+
current_frame.f_code.co_name == 1E80 "sensitive_variables_wrapper"
267+
and "sensitive_variables_wrapper" in current_frame.f_locals
268+
):
269+
# The sensitive_variables decorator was used, so take note
270+
# of the sensitive variables' names.
271+
wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
272+
sensitive_variables = getattr(wrapper, "sensitive_variables", None)
273+
break
274+
current_frame = current_frame.f_back
257275

258276
cleansed = {}
259277
if self.is_active(request) and sensitive_variables:

django/views/decorators/debug.py

Lines changed: 70 additions & 18 deletions
< 10000 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import inspect
12
from functools import wraps
23

4+
from asgiref.sync import iscoroutinefunction
5+
36
from django.http import HttpRequest
47

8+
coroutine_functions_to_sensitive_variables = {}
9+
510

611
def sensitive_variables(*variables):
712
"""
@@ -33,13 +38,42 @@ def my_function()
3338
)
3439

3540
def decorator(func):
36-
@wraps(func)
37-
def sensitive_variables_wrapper(*func_args, **func_kwargs):
41+
if iscoroutinefunction(func):
42+
43+
@wraps(func)
44+
async def sensitive_variables_wrapper(*func_args, **func_kwargs):
45+
return await func(*func_args, **func_kwargs)
46+
47+
wrapped_func = func
48+
while getattr(wrapped_func, "__wrapped__", None) is not None:
49+
wrapped_func = wrapped_func.__wrapped__
50+
51+
try:
52+
file_path = inspect.getfile(wrapped_func)
53+
_, first_file_line = inspect.getsourcelines(wrapped_func)
54+
except TypeError: # Raises for builtins or native functions.
55+
raise ValueError(
56+
f"{func.__name__} cannot safely be wrapped by "
57+
"@sensitive_variables, make it either non-async or defined in a "
58+
"Python file (not a builtin or from a native extension)."
59+
)
60+
else:
61+
key = hash(f"{file_path}:{first_file_line}")
62+
3863
if variables:
39-
sensitive_variables_wrapper.sensitive_variables = variables
64+
coroutine_functions_to_sensitive_variables[key] = variables
4065
else:
41-
sensitive_variables_wrapper.sensitive_variables = "__ALL__"
42-
return func(*func_args, **func_kwargs)
66+
coroutine_functions_to_sensitive_variables[key] = "__ALL__"
67+
68+
else:
69+
70+
@wraps(func)
71+
def sensitive_variables_wrapper(*func_args, **func_kwargs):
72+
if variables:
73+
sensitive_variables_wrapper.sensitive_variables = variables
74+
else:
75+
sensitive_variables_wrapper.sensitive_variables = "__ALL__"
76+
return func(*func_args, **func_kwargs)
4377

4478
return sensitive_variables_wrapper
4579

@@ -77,19 +111,37 @@ def my_view(request)
77111
)
78112

79113
def decorator(view):
80-
@wraps(view)
81-
def sensitive_post_parameters_wrapper(request, *args, **kwargs):
82-
if not isinstance(request, HttpRequest):
83-
raise TypeError(
84-
"sensitive_post_parameters didn't receive an HttpRequest "
85-
"object. If you are decorating a classmethod, make sure "
86-
"to use @method_decorator."
87-
)
88-
if parameters:
89-
request.sensitive_post_parameters = parameters
90-
else:
91-
request.sensitive_post_parameters = "__ALL__"
92-
return view(request, *args, **kwargs)
114+
if iscoroutinefunction(view):
115+
116+
@wraps(view)
117+
async def sensitive_post_parameters_wrapper(request, *args, **kwargs):
118+
if not isinstance(request, HttpRequest):
119+
raise TypeError(
120+
"sensitive_post_parameters didn't receive an HttpRequest "
121+
"object. If you are decorating a classmethod, make sure to use "
122+
"@method_decorator."
123+
)
124+
if parameters:
125+
request.sensitive_post_parameters = parameters
126+
else:
127+
request.sensitive_post_parameters = "__ALL__"
128+
return await view(request, *args, **kwargs)
129+
130+
else:
131+
132+
@wraps(view)
133+
def sensitive_post_parameters_wrapper(request, *args, **kwargs):
134+
if not isinstance(request, HttpRequest):
135+
raise TypeError(
136+
"sensitive_post_parameters didn't receive an HttpRequest "
137+
"object. If you are decorating a classmethod, make sure to use "
138+
"@method_decorator."
139+
)
140+
if parameters:
141+
request.sensitive_post_parameters = parameters
142+
else:
143+
request.sensitive_post_parameters = "__ALL__"
144+
return view(request, *args, **kwargs)
93145

94146
return sensitive_post_parameters_wrapper
95147

docs/howto/error-reporting.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ filtered out of error reports in a production environment (that is, where
205205
exception reporting, and consider implementing a :ref:`custom filter
206206
<custom-error-reports>` if necessary.
207207

208+
.. versionchanged:: 5.0
209+
210+
Support for wrapping ``async`` functions was added.
211+
208212
.. function:: sensitive_post_parameters(*parameters)
209213

210214
If one of your views receives an :class:`~django.http.HttpRequest` object
@@ -245,6 +249,10 @@ filtered out of error reports in a production environment (that is, where
245249
``user_change_password`` in the ``auth`` admin) to prevent the leaking of
246250
sensitive information such as user passwords.
247251

252+
.. versionchanged:: 5.0
253+
254+
Support for wrapping ``async`` functions was added.
255+
248256
.. _custom-error-reports:
249257

250258
Custom error reports

docs/releases/5.0.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ Decorators
241241
* :func:`~django.views.decorators.cache.cache_control`
242242
* :func:`~django.views.decorators.cache.never_cache`
243243
* :func:`~django.views.decorators.common.no_append_slash`
244+
* :func:`~django.views.decorators.debug.sensitive_variables`
245+
* :func:`~django.views.decorators.debug.sensitive_post_parameters`
244246
* ``xframe_options_deny()``
245247
* ``xframe_options_sameorigin()``
246248
* ``xframe_options_exempt()``
@@ -253,7 +255,9 @@ Email
253255
Error Reporting
254256
~~~~~~~~~~~~~~~
255257

256-
* ...
258+
* :func:`~django.views.decorators.debug.sensitive_variables` and
259+
:func:`~django.views.decorators.debug.sensitive_post_parameters` can now be
260+
used with asynchronous functions.
257261

258262
File Storage
259263
~~~~~~~~~~~~

tests/view_tests/tests/test_debug.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from pathlib import Path
1010
from unittest import mock, skipIf, skipUnless
1111

12+
from asgiref.sync import async_to_sync, iscoroutinefunction
13+
1214
from django.core import mail
1315
from django.core.files.uploadedfile import SimpleUploadedFile
1416
from django.db import DatabaseError, connection
@@ -39,6 +41,10 @@
3941
from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables
4042

4143
from ..views import (
44+
async_sensitive_method_view,
45+
async_sensitive_method_view_nested,
46+
async_sensitive_view,
47+
async_sensitive_view_nested,
4248
custom_exception_reporter_filter_view,
4349
index_page,
4450
multivalue_dict_key_error,
@@ -1351,7 +1357,10 @@ def verify_unsafe_response(
13511357
Asserts that potentially sensitive info are displayed in the response.
13521358
"""
13531359
request = self.rf.post("/some_url/", self.breakfast_data)
1354-
response = view(request)
1360+
if iscoroutinefunction(view):
1361+
response = async_to_sync(view)(request)
1362+
else:
1363+
response = view(request)
13551364
if check_for_vars:
13561365
# All variables are shown.
13571366
self.assertContains(response, "cooked_eggs", status_code=500)
@@ -1371,7 +1380,10 @@ def verify_safe_response(
13711380
Asserts that certain sensitive info are not displayed in the response.
13721381
"""
13731382
request = self.rf.post("/some_url/", self.breakfast_data)
1374-
response = view(request)
1383+
if iscoroutinefunction(view):
1384+
response = async_to_sync(view)(request)
1385+
else:
1386+
response = view(request)
13751387
if check_for_vars:
13761388
# Non-sensitive variable's name and value are shown.
13771389
self.assertContains(response, "cooked_eggs", status_code=500)
@@ -1418,7 +1430,10 @@ def verify_unsafe_email(self, view, check_for_POST_params=True):
14181430
with self.settings(ADMINS=[("Admin", "admin@fattie-breakie.com")]):
14191431
mail.outbox = [] # Empty outbox
14201432
request = self.rf.post("/some_url/", self.breakfast_data)
1421-
view(request)
1433+
if iscoroutinefunction(view):
1434+
async_to_sync(view)(request)
1435+
else:
1436+
view(request)
14221437
self.assertEqual(len(mail.outbox), 1)
14231438
email = mail.outbox[0]
14241439

@@ -1451,7 +1466,10 @@ def verify_safe_email(self, view, check_for_POST_params=True):
14511466
with self.settings(ADMINS=[("Admin", "admin@fattie-breakie.com")]):
14521467
mail.outbox = [] # Empty outbox
14531468
request = self.rf.post("/some_url/", self.breakfast_data)
1454-
view(request)
1469+
if iscoroutinefunction(view):
1470+
async_to_sync(view)(request)
1471+
else:
1472+
view(request)
14551473
self.assertEqual(len(mail.outbox), 1)
14561474
email = mail.outbox[0]
14571475

@@ -1543,6 +1561,24 @@ def test_sensitive_request(self):
15431561
self.verify_safe_response(sensitive_view)
15441562
self.verify_safe_email(sensitive_view)
15451563

1564+
def test_async_sensitive_request(self):
1565+
with self.settings(DEBUG=True):
1566+
self.verify_unsafe_response(async_sensitive_view)
1567+
self.verify_unsafe_email(async_sensitive_view)
1568+
1569+
with self.settings(DEBUG=False):
1570+
self.verify_safe_response(async_sensitive_view)
1571+
self.verify_safe_email(async_sensitive_view)
1572+
1573+
def test_async_sensitive_nested_request(self):
1574+
with self.settings(DEBUG=True):
1575+
self.verify_unsafe_response(async_sensitive_view_nested)
1576+
self.verify_unsafe_email(async_sensitive_view_nested)
1577+
1578+
with self.settings(DEBUG=False):
1579+
self.verify_safe_response(async_sensitive_view_nested)
1580+
self.verify_safe_email(async_sensitive_view_nested)
1581+
15461582
def test_paranoid_request(self):
15471583
"""
15481584
No POST parameters and frame variables can be seen in the
@@ -1598,6 +1634,46 @@ def test_sensitive_method(self):
15981634
)
15991635
self.verify_safe_email(sensitive_method_view, check_for_POST_params=False)
16001636

1637+
def test_async_sensitive_method(self):
1638+
"""
1639+
The sensitive_variables decorator works with async object methods.
1640+
"""
1641+
with self.settings(DEBUG=True):
1642+
self.verify_unsafe_response(
1643+
async_sensitive_method_view, check_for_POST_params=False
1644+
)
1645+
self.verify_unsafe_email(
1646+
async_sensitive_method_view, check_for_POST_params=False
1647+
)
1648+
1649+
with self.settings(DEBUG=False):
1650+
self.verify_safe_response(
1651+
async_sensitive_method_view, check_for_POST_params=False
1652+
)
1653+
self.verify_safe_email(
1654+
async_sensitive_method_view, check_for_POST_params=False
1655+
)
1656+
1657+
def test_async_sensitive_method_nested(self):
1658+
"""
1659+
The sensitive_variables decorator works with async object methods.
1660+
"""
1661+
with self.settings(DEBUG=True):
1662+
self.verify_unsafe_response(
1663+
async_sensitive_method_view_nested, check_for_POST_params=False
1664+
)
1665+
self.verify_unsafe_email(
1666+
async_sensitive_method_view_nested, check_for_POST_params=False
1667+
)
1668+
1669+
with self.settings(DEBUG=False):
1670+
self.verify_safe_response(
1671+
async_sensitive_method_view_nested, check_for_POST_params=False
1672+
)
1673+
self.verify_safe_email(
1674+
async_sensitive_method_view_nested, check_for_POST_params=False
1675+
)
1676+
16011677
def test_sensitive_function_arguments(self):
16021678
"""
16031679
Sensitive variables don't leak in the sensitive_variables decorator's
@@ -1890,6 +1966,30 @@ def test_sensitive_request(self):
18901966
with self.settings(DEBUG=False):
18911967
self.verify_safe_response(sensitive_view, check_for_vars=False)
18921968

1969+
def test_async_sensitive_request(self):
1970+
"""
1971+
Sensitive POST parameters cannot be seen in the default
1972+
error reports for sensitive requests.
1973+
"""
1974+
with self.settings(DEBUG=True):
1975+
self.verify_unsafe_response(async_sensitive_view, check_for_vars=False)
1976+
1977+
with self.settings(DEBUG=False):
1978+
self.verify_safe_response(async_sensitive_view, check_for_vars=False)
1979+
1980+
def test_async_sensitive_request_nested(self):
1981+
"""
1982+
Sensitive POST parameters cannot be seen in the default
1983+
error reports for sensitive requests.
1984+
"""
1985+
with self.settings(DEBUG=True):
1986+
self.verify_unsafe_response(
1987+
async_sensitive_view_nested, check_for_vars=False
1988+
)
1989+
1990+
with self.settings(DEBUG=False):
1991+
self.verify_safe_response(async_sensitive_view_nested, check_for_vars=False)
1992+
18931993
def test_paranoid_request(self):
18941994
"""
18951995
No POST parameters can be seen in the default error reports

0 commit comments

Comments
 (0)
0