Skip to content

Commit ff3b2a7

Browse files
authored
Use new maintained django-cors-headers package (#10000)
1 parent de223fe commit ff3b2a7

File tree

9 files changed

+101
-110
lines changed

9 files changed

+101
-110
lines changed

readthedocs/rtd_tests/tests/test_middleware.py

+46-65
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from corsheaders.middleware import (
22
ACCESS_CONTROL_ALLOW_CREDENTIALS,
33
ACCESS_CONTROL_ALLOW_ORIGIN,
4-
CorsMiddleware,
54
)
65
from django.conf import settings
76
from django.http import HttpResponse
@@ -25,8 +24,6 @@
2524
class TestCORSMiddleware(TestCase):
2625

2726
def setUp(self):
28-
self.factory = RequestFactory()
29-
self.middleware = CorsMiddleware()
3027
self.url = '/api/v2/search'
3128
self.owner = create_user(username='owner', password='test')
3229
self.project = get(
@@ -69,66 +66,60 @@ def setUp(self):
6966
)
7067

7168
def test_allow_linked_domain_from_public_version(self):
72-
request = self.factory.get(
69+
resp = self.client.get(
7370
self.url,
7471
{'project': self.project.slug, 'version': self.version.slug},
7572
HTTP_ORIGIN='http://my.valid.domain',
7673
)
77-
resp = self.middleware.process_response(request, {})
78-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
79-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
74+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
75+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
8076

8177
def test_linked_domain_from_private_version(self):
8278
self.version.privacy_level = PRIVATE
8379
self.version.save()
84-
request = self.factory.get(
80+
resp = self.client.get(
8581
self.url,
8682
{'project': self.project.slug, 'version': self.version.slug},
8783
HTTP_ORIGIN='http://my.valid.domain',
8884
)
89-
resp = self.middleware.process_response(request, {})
90-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
91-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
85+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
86+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
9287

9388
def test_allowed_api_public_version_from_another_domain(self):
94-
request = self.factory.get(
89+
resp = self.client.get(
9590
self.url,
9691
{'project': self.project.slug, 'version': self.version.slug},
9792
HTTP_ORIGIN='http://docs.another.domain',
9893
)
99-
resp = self.middleware.process_response(request, {})
100-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
101-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
94+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
95+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
10296

103-
request = self.factory.get(
97+
resp = self.client.get(
10498
self.url,
10599
{'project': self.project.slug, 'version': self.version.slug},
106100
HTTP_ORIGIN='http://another.valid.domain',
107101
)
108-
resp = self.middleware.process_response(request, {})
109-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
110-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
102+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
103+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
111104

112105
def test_api_private_version_from_another_domain(self):
113106
self.version.privacy_level = PRIVATE
114107
self.version.save()
115-
request = self.factory.get(
108+
resp = self.client.get(
116109
self.url,
117110
{'project': self.project.slug, 'version': self.version.slug},
118111
HTTP_ORIGIN='http://docs.another.domain',
119112
)
120-
resp = self.middleware.process_response(request, {})
121-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
122-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
113+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
114+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
123115

124-
request = self.factory.get(
116+
resp = self.client.get(
125117
self.url,
126118
{'project': self.project.slug, 'version': self.version.slug},
127119
HTTP_ORIGIN='http://another.valid.domain',
128120
)
129-
resp = self.middleware.process_response(request, {})
130-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
131-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
121+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
122+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
132123

133124
def test_valid_subproject(self):
134125
self.assertTrue(
@@ -137,102 +128,92 @@ def test_valid_subproject(self):
137128
subprojects__child=self.subproject,
138129
).exists(),
139130
)
140-
request = self.factory.get(
131+
resp = self.client.get(
141132
self.url,
142133
{'project': self.project.slug, 'version': self.version.slug},
143134
HTTP_ORIGIN='http://my.valid.domain',
144135
)
145-
resp = self.middleware.process_response(request, {})
146-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
147-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
136+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
137+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
148138

149139
def test_embed_api_private_version_linked_domain(self):
150140
self.version.privacy_level = PRIVATE
151141
self.version.save()
152-
request = self.factory.get(
142+
resp = self.client.get(
153143
'/api/v2/embed/',
154144
{'project': self.project.slug, 'version': self.version.slug},
155145
HTTP_ORIGIN='http://my.valid.domain',
156146
)
157-
resp = self.middleware.process_response(request, {})
158-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
159-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
147+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
148+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
160149

161150
def test_embed_api_external_url(self):
162-
request = self.factory.get(
151+
resp = self.client.get(
163152
"/api/v2/embed/",
164153
{"url": "https://pip.readthedocs.io/en/latest/index.hml"},
165154
HTTP_ORIGIN="http://my.valid.domain",
166155
)
167-
resp = self.middleware.process_response(request, {})
168-
self.assertIn("Access-Control-Allow-Origin", resp)
156+
self.assertIn("Access-Control-Allow-Origin", resp.headers)
169157

170-
request = self.factory.get(
158+
resp = self.client.get(
171159
"/api/v2/embed/",
172160
{"url": "https://docs.example.com/en/latest/index.hml"},
173161
HTTP_ORIGIN="http://my.valid.domain",
174162
)
175-
resp = self.middleware.process_response(request, {})
176-
self.assertIn("Access-Control-Allow-Origin", resp)
163+
self.assertIn("Access-Control-Allow-Origin", resp.headers)
177164

178165
def test_sustainability_endpoint_allways_allowed(self):
179-
request = self.factory.get(
166+
resp = self.client.get(
180167
'/api/v2/sustainability/',
181168
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
182169
HTTP_ORIGIN='http://invalid.domain',
183170
)
184-
resp = self.middleware.process_response(request, {})
185-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
186-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
171+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
172+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
187173

188-
request = self.factory.get(
174+
resp = self.client.get(
189175
'/api/v2/sustainability/',
190176
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
191177
HTTP_ORIGIN='http://my.valid.domain',
192178
)
193-
resp = self.middleware.process_response(request, {})
194-
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
195-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
179+
self.assertIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
180+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
196181

197182
def test_apiv2_endpoint_not_allowed(self):
198-
request = self.factory.get(
183+
resp = self.client.get(
199184
'/api/v2/version/',
200185
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
201186
HTTP_ORIGIN='http://invalid.domain',
202187
)
203-
resp = self.middleware.process_response(request, {})
204-
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
205-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
188+
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
189+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
206190

207191
# This also doesn't work on registered domains.
208-
request = self.factory.get(
192+
resp = self.client.get(
209193
'/api/v2/version/',
210194
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
211195
HTTP_ORIGIN='http://my.valid.domain',
212196
)
213-
resp = self.middleware.process_response(request, {})
214-
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
215-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
197+
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
198+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
216199

217200
# Or from our public domain.
218-
request = self.factory.get(
201+
resp = self.client.get(
219202
'/api/v2/version/',
220203
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
221204
HTTP_ORIGIN='http://docs.readthedocs.io/',
222205
)
223-
resp = self.middleware.process_response(request, {})
224-
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
225-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
206+
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
207+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
226208

227209
# POST is not allowed
228-
request = self.factory.post(
210+
resp = self.client.post(
229211
'/api/v2/version/',
230212
{'project': self.project.slug, 'active': True, 'version': self.version.slug},
231213
HTTP_ORIGIN='http://my.valid.domain',
232214
)
233-
resp = self.middleware.process_response(request, {})
234-
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp)
235-
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp)
215+
self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers)
216+
self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, resp.headers)
236217

237218

238219
class TestSessionMiddleware(TestCase):

readthedocs/settings/base.py

+26-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# pylint: disable=missing-docstring
22

33
import os
4+
import re
45
import subprocess
56
import socket
67

@@ -9,6 +10,7 @@
910
from celery.schedules import crontab
1011

1112
from readthedocs.core.logs import shared_processors
13+
from corsheaders.defaults import default_headers
1214
from readthedocs.core.settings import Settings
1315

1416

@@ -277,14 +279,14 @@ def MIDDLEWARE(self):
277279
'readthedocs.core.middleware.NullCharactersMiddleware',
278280
'readthedocs.core.middleware.ReadTheDocsSessionMiddleware',
279281
'django.middleware.locale.LocaleMiddleware',
282+
'corsheaders.middleware.CorsMiddleware',
280283
'django.middleware.common.CommonMiddleware',
281284
'django.middleware.security.SecurityMiddleware',
282285
'django.middleware.csrf.CsrfViewMiddleware',
283286
'django.middleware.clickjacking.XFrameOptionsMiddleware',
284287
'django.contrib.auth.middleware.AuthenticationMiddleware',
285288
'django.contrib.messages.middleware.MessageMiddleware',
286289
'dj_pagination.middleware.PaginationMiddleware',
287-
'corsheaders.middleware.CorsMiddleware',
288290
'csp.middleware.CSPMiddleware',
289291
'readthedocs.core.middleware.ReferrerPolicyMiddleware',
290292
'simple_history.middleware.HistoryRequestMiddleware',
@@ -734,15 +736,17 @@ def DOCKER_LIMITS(self):
734736
# users to CSRF attacks. The sustainability API is the only view that requires
735737
# cookies to be send cross-site, we override that for that view only.
736738
CORS_ALLOW_CREDENTIALS = False
737-
CORS_ALLOW_HEADERS = (
738-
'x-requested-with',
739-
'content-type',
740-
'accept',
741-
'origin',
742-
'authorization',
739+
740+
# Allow cross-site requests from any origin,
741+
# all information from our allowed endpoits is public.
742+
#
743+
# NOTE: We don't use `CORS_ALLOW_ALL_ORIGINS=True`,
744+
# since that will set the `Access-Control-Allow-Origin` header to `*`,
745+
# we won't be able to pass credentials fo the sustainability API with that value.
746+
CORS_ALLOWED_ORIGIN_REGEXES = [re.compile(".+")]
747+
CORS_ALLOW_HEADERS = list(default_headers) + [
743748
'x-hoverxref-version',
744-
'x-csrftoken'
745-
)
749+
]
746750
# Additional protection to allow only idempotent methods.
747751
CORS_ALLOW_METHODS = [
748752
'GET',
@@ -751,14 +755,19 @@ def DOCKER_LIMITS(self):
751755
]
752756

753757
# URLs to allow CORS to read from unauthed.
754-
CORS_URLS_ALLOW_ALL_REGEX = [
755-
r"^/api/v2/footer_html",
756-
r"^/api/v2/search",
757-
r"^/api/v2/docsearch",
758-
r"^/api/v2/embed",
759-
r"^/api/v3/embed",
760-
r"^/api/v2/sustainability",
761-
]
758+
CORS_URLS_REGEX = re.compile(
759+
r"""
760+
^(
761+
/api/v2/footer_html
762+
|/api/v2/search
763+
|/api/v2/docsearch
764+
|/api/v2/embed
765+
|/api/v3/embed
766+
|/api/v2/sustainability
767+
)
768+
""",
769+
re.VERBOSE,
770+
)
762771

763772
# RTD Settings
764773
ALLOW_PRIVATE_REPOS = False

readthedocs/settings/proxito/base.py

-3
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ class CommunityProxitoSettingsMixin:
2020
# As 'Lax' breaks when the page is embedded in an iframe.
2121
SESSION_COOKIE_SAMESITE = None
2222

23-
# We don't need or want to allow cross site requests in proxito.
24-
CORS_URLS_ALLOW_ALL_REGEX = []
25-
2623
@property
2724
def DATABASES(self):
2825
# This keeps connections to the DB alive,

requirements/deploy.txt

+6-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ billiard==3.6.4.0
2828
# via
2929
# -r requirements/pip.txt
3030
# celery
31-
boto3==1.26.64
31+
boto3==1.26.65
3232
# via
3333
# -r requirements/pip.txt
3434
# django-storages
35-
botocore==1.29.64
35+
botocore==1.29.65
3636
# via
3737
# -r requirements/pip.txt
3838
# boto3
@@ -104,6 +104,7 @@ django==3.2.17
104104
# dj-stripe
105105
# django-allauth
106106
# django-annoying
107+
# django-cors-headers
107108
# django-csp
108109
# django-debug-toolbar
109110
# django-extensions
@@ -121,7 +122,7 @@ django-annoying==0.10.6
121122
# via -r requirements/pip.txt
122123
django-autoslug==1.9.8
123124
# via -r requirements/pip.txt
124-
django-cors-middleware==1.4.0
125+
django-cors-headers==3.13.0
125126
# via -r requirements/pip.txt
126127
django-crispy-forms==1.14.0
127128
# via -r requirements/pip.txt
@@ -338,7 +339,7 @@ s3transfer==0.6.0
338339
# boto3
339340
selectolax==0.3.12
340341
# via -r requirements/pip.txt
341-
sentry-sdk==1.14.0
342+
sentry-sdk==1.15.0
342343
# via structlog-sentry
343344
six==1.16.0
344345
# via
@@ -438,7 +439,7 @@ vine==5.0.0
438439
# amqp
439440
# celery
440441
# kombu
441-
virtualenv==20.17.1
442+
virtualenv==20.18.0
442443
# via -r requirements/pip.txt
443444
wcwidth==0.2.6
444445
# via

0 commit comments

Comments
 (0)