Skip to content

Commit 0f9984a

Browse files
authored
fix(django): Un-break csrf_exempt (getsentry#791)
1 parent dea47a1 commit 0f9984a

File tree

5 files changed

+104
-6
lines changed

5 files changed

+104
-6
lines changed

sentry_sdk/integrations/django/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from sentry_sdk.hub import Hub
22
from sentry_sdk._types import MYPY
3-
from sentry_sdk._functools import wraps
3+
from sentry_sdk import _functools
44

55
if MYPY:
66
from typing import Any
@@ -43,7 +43,20 @@ def _wrap_resolver_match(hub, resolver_match):
4343

4444
old_callback = resolver_match.func
4545

46-
@wraps(old_callback)
46+
# Explicitly forward `csrf_exempt` in case it is not an attribute in
47+
# callback.__dict__, but rather a class attribute (on a class
48+
# implementing __call__) such as this:
49+
#
50+
# class Foo(object):
51+
# csrf_exempt = True
52+
#
53+
# def __call__(self, request): ...
54+
#
55+
# We have had this in the Sentry codebase (for no good reason, but
56+
# nevertheless we broke user code)
57+
assigned = _functools.WRAPPER_ASSIGNMENTS + ("csrf_exempt",)
58+
59+
@_functools.wraps(old_callback, assigned=assigned)
4760
def callback(*args, **kwargs):
4861
# type: (*Any, **Any) -> Any
4962
with hub.start_span(op="django.view", description=resolver_match.view_name):

tests/integrations/django/myapp/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def middleware(request):
7676
MIDDLEWARE_CLASSES = [
7777
"django.contrib.sessions.middleware.SessionMiddleware",
7878
"django.contrib.auth.middleware.AuthenticationMiddleware",
79+
"django.middleware.csrf.CsrfViewMiddleware",
7980
"tests.integrations.django.myapp.settings.TestMiddleware",
8081
]
8182

tests/integrations/django/myapp/urls.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
try:
1919
from django.urls import path
2020
except ImportError:
21-
from django.conf.urls import url as path
21+
from django.conf.urls import url
22+
23+
def path(path, *args, **kwargs):
24+
return url("^{}$".format(path), *args, **kwargs)
25+
2226

2327
from . import views
2428

@@ -33,13 +37,24 @@
3337
path("message", views.message, name="message"),
3438
path("mylogin", views.mylogin, name="mylogin"),
3539
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
40+
path("sentryclass", views.SentryClassBasedView(), name="sentryclass"),
41+
path(
42+
"sentryclass-csrf",
43+
views.SentryClassBasedViewWithCsrf(),
44+
name="sentryclass_csrf",
45+
),
3646
path("post-echo", views.post_echo, name="post_echo"),
3747
path("template-exc", views.template_exc, name="template_exc"),
3848
path(
3949
"permission-denied-exc",
4050
views.permission_denied_exc,
4151
name="permission_denied_exc",
4252
),
53+
path(
54+
"csrf-hello-not-exempt",
55+
views.csrf_hello_not_exempt,
56+
name="csrf_hello_not_exempt",
57+
),
4358
]
4459

4560

tests/integrations/django/myapp/views.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
55
from django.shortcuts import render
66
from django.views.generic import ListView
7+
from django.views.decorators.csrf import csrf_exempt
8+
from django.utils.decorators import method_decorator
79

810
try:
911
from rest_framework.decorators import api_view
@@ -33,52 +35,88 @@ def rest_permission_denied_exc(request):
3335
import sentry_sdk
3436

3537

38+
@csrf_exempt
3639
def view_exc(request):
3740
1 / 0
3841

3942

43+
# This is a "class based view" as previously found in the sentry codebase. The
44+
# interesting property of this one is that csrf_exempt, as a class attribute,
45+
# is not in __dict__, so regular use of functools.wraps will not forward the
46+
# attribute.
47+
class SentryClassBasedView(object):
48+
csrf_exempt = True
49+
50+
def __call__(self, request):
51+
return HttpResponse("ok")
52+
53+
54+
class SentryClassBasedViewWithCsrf(object):
55+
def __call__(self, request):
56+
return HttpResponse("ok")
57+
58+
59+
@csrf_exempt
4060
def read_body_and_view_exc(request):
4161
request.read()
4262
1 / 0
4363

4464

65+
@csrf_exempt
4566
def message(request):
4667
sentry_sdk.capture_message("hi")
4768
return HttpResponse("ok")
4869

4970

71+
@csrf_exempt
5072
def mylogin(request):
5173
user = User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
5274
user.backend = "django.contrib.auth.backends.ModelBackend"
5375
login(request, user)
5476
return HttpResponse("ok")
5577

5678

79+
@csrf_exempt
5780
def handler500(request):
5881
return HttpResponseServerError("Sentry error: %s" % sentry_sdk.last_event_id())
5982

6083

6184
class ClassBasedView(ListView):
6285
model = None
6386

87+
@method_decorator(csrf_exempt)
88+
def dispatch(self, request, *args, **kwargs):
89+
return super(ClassBasedView, self).dispatch(request, *args, **kwargs)
90+
6491
def head(self, *args, **kwargs):
6592
sentry_sdk.capture_message("hi")
6693
return HttpResponse("")
6794

95+
def post(self, *args, **kwargs):
96+
return HttpResponse("ok")
97+
6898

99+
@csrf_exempt
69100
def post_echo(request):
70101
sentry_sdk.capture_message("hi")
71102
return HttpResponse(request.body)
72103

73104

105+
@csrf_exempt
74106
def handler404(*args, **kwargs):
75107
sentry_sdk.capture_message("not found", level="error")
76108
return HttpResponseNotFound("404")
77109

78110

111+
@csrf_exempt
79112
def template_exc(request, *args, **kwargs):
80113
return render(request, "error.html")
81114

82115

116+
@csrf_exempt
83117
def permission_denied_exc(*args, **kwargs):
84118
raise PermissionDenied("bye")
119+
120+
121+
def csrf_hello_not_exempt(*args, **kwargs):
122+
return HttpResponse("ok")

tests/integrations/django/test_basic.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,9 +532,11 @@ def test_middleware_spans(sentry_init, client, capture_events, render_span_tree)
532532
- op="http.server": description=null
533533
- op="django.middleware": description="django.contrib.sessions.middleware.SessionMiddleware.__call__"
534534
- op="django.middleware": description="django.contrib.auth.middleware.AuthenticationMiddleware.__call__"
535-
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestMiddleware.__call__"
536-
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__"
537-
- op="django.view": description="message"\
535+
- op="django.middleware": description="django.middleware.csrf.CsrfViewMiddleware.__call__"
536+
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestMiddleware.__call__"
537+
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__"
538+
- op="django.middleware": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
539+
- op="django.view": description="message"\
538540
"""
539541
)
540542

@@ -546,8 +548,10 @@ def test_middleware_spans(sentry_init, client, capture_events, render_span_tree)
546548
- op="django.middleware": description="django.contrib.sessions.middleware.SessionMiddleware.process_request"
547549
- op="django.middleware": description="django.contrib.auth.middleware.AuthenticationMiddleware.process_request"
548550
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestMiddleware.process_request"
551+
- op="django.middleware": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
549552
- op="django.view": description="message"
550553
- op="django.middleware": description="tests.integrations.django.myapp.settings.TestMiddleware.process_response"
554+
- op="django.middleware": description="django.middleware.csrf.CsrfViewMiddleware.process_response"
551555
- op="django.middleware": description="django.contrib.sessions.middleware.SessionMiddleware.process_response"\
552556
"""
553557
)
@@ -566,3 +570,30 @@ def test_middleware_spans_disabled(sentry_init, client, capture_events):
566570
assert message["message"] == "hi"
567571

568572
assert not transaction["spans"]
573+
574+
575+
def test_csrf(sentry_init, client):
576+
"""
577+
Assert that CSRF view decorator works even with the view wrapped in our own
578+
callable.
579+
"""
580+
581+
sentry_init(integrations=[DjangoIntegration()])
582+
583+
content, status, _headers = client.post(reverse("csrf_hello_not_exempt"))
584+
assert status.lower() == "403 forbidden"
585+
586+
content, status, _headers = client.post(reverse("sentryclass_csrf"))
587+
assert status.lower() == "403 forbidden"
588+
589+
content, status, _headers = client.post(reverse("sentryclass"))
590+
assert status.lower() == "200 ok"
591+
assert b"".join(content) == b"ok"
592+
593+
content, status, _headers = client.post(reverse("classbased"))
594+
assert status.lower() == "200 ok"
595+
assert b"".join(content) == b"ok"
596+
597+
content, status, _headers = client.post(reverse("message"))
598+
assert status.lower() == "200 ok"
599+
assert b"".join(content) == b"ok"

0 commit comments

Comments
 (0)