1import inspect
2from functools import wraps
3
4from asgiref.sync import iscoroutinefunction
5
6from django.http import HttpRequest
7
8coroutine_functions_to_sensitive_variables = {}
9
10
11def sensitive_variables(*variables):
12 """
13 Indicate which variables used in the decorated function are sensitive so
14 that those variables can later be treated in a special way, for example
15 by hiding them when logging unhandled exceptions.
16
17 Accept two forms:
18
19 * with specified variable names:
20
21 @sensitive_variables('user', 'password', 'credit_card')
22 def my_function(user):
23 password = user.pass_word
24 credit_card = user.credit_card_number
25 ...
26
27 * without any specified variable names, in which case consider all
28 variables are sensitive:
29
30 @sensitive_variables()
31 def my_function()
32 ...
33 """
34 if len(variables) == 1 and callable(variables[0]):
35 raise TypeError(
36 "sensitive_variables() must be called to use it as a decorator, "
37 "e.g., use @sensitive_variables(), not @sensitive_variables."
38 )
39
40 def decorator(func):
41 if iscoroutinefunction(func):
42 sensitive_variables_wrapper = func
43
44 wrapped_func = func
45 while getattr(wrapped_func, "__wrapped__", None) is not None:
46 wrapped_func = wrapped_func.__wrapped__
47
48 try:
49 file_path = inspect.getfile(wrapped_func)
50 except TypeError: # Raises for builtins or native functions.
51 raise ValueError(
52 f"{func.__name__} cannot safely be wrapped by "
53 "@sensitive_variables, make it either non-async or defined in a "
54 "Python file (not a builtin or from a native extension)."
55 )
56 else:
57 # A source file may not be available (e.g. in .pyc-only builds),
58 # use the first line number instead.
59 first_line_number = wrapped_func.__code__.co_firstlineno
60 key = hash(f"{file_path}:{first_line_number}")
61
62 if variables:
63 coroutine_functions_to_sensitive_variables[key] = variables
64 else:
65 coroutine_functions_to_sensitive_variables[key] = "__ALL__"
66
67 else:
68
69 @wraps(func)
70 def sensitive_variables_wrapper(*func_args, **func_kwargs):
71 if variables:
72 sensitive_variables_wrapper.sensitive_variables = variables
73 else:
74 sensitive_variables_wrapper.sensitive_variables = "__ALL__"
75 return func(*func_args, **func_kwargs)
76
77 return sensitive_variables_wrapper
78
79 return decorator
80
81
82def sensitive_post_parameters(*parameters):
83 """
84 Indicate which POST parameters used in the decorated view are sensitive,
85 so that those parameters can later be treated in a special way, for example
86 by hiding them when logging unhandled exceptions.
87
88 Accept two forms:
89
90 * with specified parameters:
91
92 @sensitive_post_parameters('password', 'credit_card')
93 def my_view(request):
94 pw = request.POST['password']
95 cc = request.POST['credit_card']
96 ...
97
98 * without any specified parameters, in which case consider all
99 variables are sensitive:
100
101 @sensitive_post_parameters()
102 def my_view(request)
103 ...
104 """
105 if len(parameters) == 1 and callable(parameters[0]):
106 raise TypeError(
107 "sensitive_post_parameters() must be called to use it as a "
108 "decorator, e.g., use @sensitive_post_parameters(), not "
109 "@sensitive_post_parameters."
110 )
111
112 def decorator(view):
113 if iscoroutinefunction(view):
114
115 @wraps(view)
116 async def sensitive_post_parameters_wrapper(request, *args, **kwargs):
117 if not isinstance(request, HttpRequest):
118 raise TypeError(
119 "sensitive_post_parameters didn't receive an HttpRequest "
120 "object. If you are decorating a classmethod, make sure to use "
121 "@method_decorator."
122 )
123 if parameters:
124 request.sensitive_post_parameters = parameters
125 else:
126 request.sensitive_post_parameters = "__ALL__"
127 return await view(request, *args, **kwargs)
128
129 else:
130
131 @wraps(view)
132 def sensitive_post_parameters_wrapper(request, *args, **kwargs):
133 if not isinstance(request, HttpRequest):
134 raise TypeError(
135 "sensitive_post_parameters didn't receive an HttpRequest "
136 "object. If you are decorating a classmethod, make sure to use "
137 "@method_decorator."
138 )
139 if parameters:
140 request.sensitive_post_parameters = parameters
141 else:
142 request.sensitive_post_parameters = "__ALL__"
143 return view(request, *args, **kwargs)
144
145 return sensitive_post_parameters_wrapper
146
147 return decorator