Skip to content

Commit 731b4cd

Browse files
committed
Improve CORS configuration
The goal of this commit is to allow access to our resources from different hosts: * everything coming from `*.readthedocs.io` * everything hitting any URL under `/api/` if it's safe method, no matter the host where the request comes from Compared with the previous implementation, this reduces the number of queries to our database since we are not checking that the host where the request was made is a known Domain for us. Besides, this opens access to `/api/v3` as well.
1 parent 72972b5 commit 731b4cd

File tree

3 files changed

+36
-65
lines changed

3 files changed

+36
-65
lines changed

readthedocs/core/signals.py

+6-51
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@
1818

1919
log = logging.getLogger(__name__)
2020

21-
WHITELIST_URLS = [
22-
'/api/v2/footer_html',
23-
'/api/v2/search',
24-
'/api/v2/docsearch',
25-
'/api/v2/sustainability',
26-
]
27-
2821
webhook_github = Signal(providing_args=['project', 'data', 'event'])
2922
webhook_gitlab = Signal(providing_args=['project', 'data', 'event'])
3023
webhook_bitbucket = Signal(providing_args=['project', 'data', 'event'])
@@ -34,52 +27,14 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen
3427
"""
3528
Decide whether a request should be given CORS access.
3629
37-
This checks that:
38-
* The URL is whitelisted against our CORS-allowed domains
39-
* The Domain exists in our database, and belongs to the project being queried.
30+
This checks that the URL is under ``/api/`` and it's a safe method.
4031
41-
Returns True when a request should be given CORS access.
32+
Returns ``True`` when a request should be given CORS access.
4233
"""
43-
if 'HTTP_ORIGIN' not in request.META:
44-
return False
45-
host = urlparse(request.META['HTTP_ORIGIN']).netloc.split(':')[0]
46-
47-
# Don't do domain checking for this API for now
48-
if request.path_info.startswith('/api/v2/sustainability'):
49-
return True
50-
51-
# Don't do domain checking for APIv2 when the Domain is known
52-
if request.path_info.startswith('/api/v2/') and request.method in SAFE_METHODS:
53-
domain = Domain.objects.filter(domain__icontains=host)
54-
if domain.exists():
55-
return True
56-
57-
valid_url = False
58-
for url in WHITELIST_URLS:
59-
if request.path_info.startswith(url):
60-
valid_url = True
61-
break
62-
63-
if valid_url:
64-
project_slug = request.GET.get('project', None)
65-
try:
66-
project = Project.objects.get(slug=project_slug)
67-
except Project.DoesNotExist:
68-
log.warning(
69-
'Invalid project passed to domain. [%s:%s]',
70-
project_slug,
71-
host,
72-
)
73-
return False
74-
75-
domain = Domain.objects.filter(
76-
Q(domain__icontains=host),
77-
Q(project=project) | Q(project__subprojects__child=project),
78-
)
79-
if domain.exists():
80-
return True
81-
82-
return False
34+
return all([
35+
request.method in SAFE_METHODS,
36+
request.path_info.startswith('/api/'),
37+
])
8338

8439

8540
@receiver(pre_delete, sender=settings.AUTH_USER_MODEL)

readthedocs/rtd_tests/tests/test_middleware.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def setUp(self):
185185
)
186186
self.domain = get(Domain, domain='my.valid.domain', project=self.project)
187187

188-
def test_proper_domain(self):
188+
def test_known_domain(self):
189189
request = self.factory.get(
190190
self.url,
191191
{'project': self.project.slug},
@@ -194,11 +194,24 @@ def test_proper_domain(self):
194194
resp = self.middleware.process_response(request, {})
195195
self.assertIn('Access-Control-Allow-Origin', resp)
196196

197-
def test_invalid_domain(self):
197+
def test_unknown_domain(self):
198+
"""
199+
Test accessing the API from a domain that it's not registered in our DB.
200+
201+
Only the endpoints under ``/api/`` should work.
202+
"""
198203
request = self.factory.get(
199204
self.url,
200205
{'project': self.project.slug},
201-
HTTP_ORIGIN='http://invalid.domain',
206+
HTTP_ORIGIN='http://unknown.domain',
207+
)
208+
resp = self.middleware.process_response(request, {})
209+
self.assertIn('Access-Control-Allow-Origin', resp)
210+
211+
request = self.factory.get(
212+
'/dashboard/',
213+
{'project': self.project.slug},
214+
HTTP_ORIGIN='http://unknown.domain',
202215
)
203216
resp = self.middleware.process_response(request, {})
204217
self.assertNotIn('Access-Control-Allow-Origin', resp)
@@ -227,15 +240,15 @@ def test_apiv2_endpoint_allowed(self):
227240
resp = self.middleware.process_response(request, {})
228241
self.assertIn('Access-Control-Allow-Origin', resp)
229242

230-
def test_apiv2_endpoint_not_allowed(self):
231243
request = self.factory.get(
232244
'/api/v2/version/',
233245
{'project__slug': self.project.slug, 'active': True},
234246
HTTP_ORIGIN='http://invalid.domain',
235247
)
236248
resp = self.middleware.process_response(request, {})
237-
self.assertNotIn('Access-Control-Allow-Origin', resp)
249+
self.assertIn('Access-Control-Allow-Origin', resp)
238250

251+
def test_apiv2_method_not_allowed(self):
239252
# POST is not allowed
240253
request = self.factory.post(
241254
'/api/v2/version/',

readthedocs/settings/base.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -403,16 +403,19 @@ def USE_PROMOS(self): # noqa
403403
r'^http://(.+)\.readthedocs\.io$',
404404
r'^https://(.+)\.readthedocs\.io$',
405405
)
406-
# So people can post to their accounts
406+
407+
# We need to allow credentials (cookies) to identify the user and remove ads
408+
# if it's Gold member. Note: in Django 2.1 the ``SESSION_COOKIE_SAMESITE``
409+
# setting was added, set to 'Lax' by default, which will prevent Django's
410+
# session cookie being sent cross-domain. Change it to None to bypass this
411+
# security restriction.
407412
CORS_ALLOW_CREDENTIALS = True
408-
CORS_ALLOW_HEADERS = (
409-
'x-requested-with',
410-
'content-type',
411-
'accept',
412-
'origin',
413-
'authorization',
414-
'x-csrftoken'
415-
)
413+
414+
CORS_ALLOW_METHODS = [
415+
'GET',
416+
'OPTIONS',
417+
'HEAD',
418+
]
416419

417420
# RTD Settings
418421
REPO_LOCK_SECONDS = 30

0 commit comments

Comments
 (0)