diff --git a/docs/server-side-search.rst b/docs/server-side-search.rst index 4be97103024..74299337c7d 100644 --- a/docs/server-side-search.rst +++ b/docs/server-side-search.rst @@ -72,14 +72,15 @@ and then click on :guilabel:`Search Analytics`. API --- -Search is exposed through our API that's proxied from the domain where your docs are being served. -This is ``https://docs.readthedocs.io/_/api/v2/search`` for the ``docs`` project, for example. +If you are using :doc:`/commercial/index` you will need to replace +https://readthedocs.org/ with https://readthedocs.com/ in all the URLs used in the following examples. +Check :ref:`server-side-search:authentication and authorization` if you are using private versions. .. warning:: This API isn't stable yet, some small things may change in the future. -.. http:get:: /_/api/v2/search/ +.. http:get:: /api/v2/search/ Return a list of search results for a project, including results from its :doc:`/subprojects`. @@ -120,12 +121,12 @@ This is ``https://docs.readthedocs.io/_/api/v2/search`` for the ``docs`` project .. code-tab:: bash - $ curl "https://docs.readthedocs.io/_/api/v2/search/?project=docs&version=latest&q=server%20side%20search" + $ curl "https://readthedocs.org/api/v2/search/?project=docs&version=latest&q=server%20side%20search" .. code-tab:: python import requests - URL = 'https://docs.readthedocs.io/_/api/v2/search/' + URL = 'https://readthedocs.org/api/v2/search/' params = { 'q': 'server side search', 'project': 'docs', @@ -140,7 +141,7 @@ This is ``https://docs.readthedocs.io/_/api/v2/search`` for the ``docs`` project { "count": 41, - "next": "https://docs.readthedocs.io/api/v2/search/?page=2&project=read-the-docs&q=server+side+search&version=latest", + "next": "https://readthedocs.org/api/v2/search/?page=2&project=read-the-docs&q=server+side+search&version=latest", "previous": null, "results": [ { @@ -194,3 +195,8 @@ If you are using :ref:`private versions `, users will only be allowed to search projects they have permissions over. Authentication and authorization is done using the current session, or any of the valid :doc:`sharing methods `. + +To be able to use the user's current session you need to use the API from the domain where your docs are being served +(``/_/api/v2/search/``). +This is ``https://docs.readthedocs-hosted.com/_/api/v2/search/`` +for the ``https://docs.readthedocs-hosted.com/`` project, for example. diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 5b48fb2f8de..a768756ce52 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -2,22 +2,16 @@ import time from django.conf import settings -from django.contrib.sessions.backends.base import SessionBase -from django.contrib.sessions.backends.base import UpdateError +from django.contrib.sessions.backends.base import SessionBase, UpdateError from django.contrib.sessions.middleware import SessionMiddleware -from django.core.exceptions import SuspiciousOperation -from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist -from django.http import Http404, HttpResponseBadRequest -from django.urls.base import set_urlconf +from django.core.exceptions import ( + ImproperlyConfigured, + MiddlewareNotUsed, + SuspiciousOperation, +) from django.utils.cache import patch_vary_headers -from django.utils.deprecation import MiddlewareMixin from django.utils.http import http_date from django.utils.translation import ugettext_lazy as _ -from django.shortcuts import render - -from readthedocs.projects.models import Domain, Project - log = logging.getLogger(__name__) diff --git a/readthedocs/core/signals.py b/readthedocs/core/signals.py index 9010859e3f4..23c0ac65d9f 100644 --- a/readthedocs/core/signals.py +++ b/readthedocs/core/signals.py @@ -1,11 +1,10 @@ """Signal handling for core app.""" import logging -from urllib.parse import urlparse from corsheaders import signals from django.conf import settings -from django.db.models import Count, Q +from django.db.models import Count from django.db.models.signals import pre_delete from django.dispatch import Signal, receiver from rest_framework.permissions import SAFE_METHODS @@ -13,7 +12,7 @@ from readthedocs.builds.models import Version from readthedocs.core.unresolver import unresolve -from readthedocs.projects.models import Domain, Project +from readthedocs.projects.models import Project log = logging.getLogger(__name__) @@ -50,8 +49,7 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen * It's a safe HTTP method * The origin is in ALLOWED_URLS * The URL is owned by the project that they are requesting data from - * The version is public or the domain is linked to the project - (except for the embed API). + * The version is public .. note:: @@ -63,8 +61,6 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen if 'HTTP_ORIGIN' not in request.META or request.method not in SAFE_METHODS: return False - host = urlparse(request.META['HTTP_ORIGIN']).netloc.split(':')[0] - # Always allow the sustainability API, # it's used only on .org to check for ad-free users. if _has_donate_app() and request.path_info.startswith('/api/v2/sustainability'): @@ -104,20 +100,6 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen if is_public: return True - # Don't check for known domains for the embed api. - # It gives a lot of information, - # we should use a list of trusted domains from the user. - if valid_url == '/api/v2/embed': - return False - - # Or allow if they have a registered domain - # linked to that project. - domain = Domain.objects.filter( - Q(domain__iexact=host), - Q(project=project) | Q(project__subprojects__child=project), - ) - if domain.exists(): - return True return False diff --git a/readthedocs/rtd_tests/tests/test_middleware.py b/readthedocs/rtd_tests/tests/test_middleware.py index c0ea03ccbd6..3c143e1b900 100644 --- a/readthedocs/rtd_tests/tests/test_middleware.py +++ b/readthedocs/rtd_tests/tests/test_middleware.py @@ -72,7 +72,7 @@ def test_allow_linked_domain_from_public_version(self): resp = self.middleware.process_response(request, {}) self.assertIn('Access-Control-Allow-Origin', resp) - def test_allow_linked_domain_from_private_version(self): + def test_dont_allow_linked_domain_from_private_version(self): self.version.privacy_level = PRIVATE self.version.save() request = self.factory.get( @@ -81,7 +81,7 @@ def test_allow_linked_domain_from_private_version(self): HTTP_ORIGIN='http://my.valid.domain', ) resp = self.middleware.process_response(request, {}) - self.assertIn('Access-Control-Allow-Origin', resp) + self.assertNotIn('Access-Control-Allow-Origin', resp) def test_allowed_api_public_version_from_another_domain(self): request = self.factory.get( @@ -228,6 +228,7 @@ def setUp(self): self.user = create_user(username='owner', password='test') + @override_settings(SESSION_COOKIE_SAMESITE=None) def test_fallback_cookie(self): request = self.factory.get('/') response = HttpResponse() @@ -238,6 +239,7 @@ def test_fallback_cookie(self): self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies) self.assertTrue(self.middleware.cookie_name_fallback in response.cookies) + @override_settings(SESSION_COOKIE_SAMESITE=None) def test_main_cookie_samesite_none(self): request = self.factory.get('/') response = HttpResponse() @@ -247,3 +249,13 @@ def test_main_cookie_samesite_none(self): self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'None') self.assertEqual(response.cookies[self.middleware.cookie_name_fallback]['samesite'], '') + + def test_main_cookie_samesite_lax(self): + request = self.factory.get('/') + response = HttpResponse() + self.middleware.process_request(request) + request.session['test'] = 'value' + response = self.middleware.process_response(request, response) + + self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'Lax') + self.assertTrue(self.test_main_cookie_samesite_none not in response.cookies) diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 18ef96e13e4..c07af4e1c70 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -73,8 +73,16 @@ class CommunityBaseSettings(Settings): SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_AGE = 30 * 24 * 60 * 60 # 30 days SESSION_SAVE_EVERY_REQUEST = True - # This cookie is used in cross-origin API requests from *.readthedocs.io to readthedocs.org - SESSION_COOKIE_SAMESITE = None + + @property + def SESSION_COOKIE_SAMESITE(self): + """ + Cookie used in cross-origin API requests from *.rtd.io to rtd.org/api/v2/sustainability/. + """ + if self.USE_PROMOS: + return None + # This is django's default. + return 'Lax' # CSRF CSRF_COOKIE_HTTPONLY = True diff --git a/readthedocs/settings/proxito/base.py b/readthedocs/settings/proxito/base.py index 9ab99e58a49..774560f0b70 100644 --- a/readthedocs/settings/proxito/base.py +++ b/readthedocs/settings/proxito/base.py @@ -13,6 +13,12 @@ class CommunityProxitoSettingsMixin: USE_SUBDOMAIN = True SECURE_REFERRER_POLICY = "no-referrer-when-downgrade" + # Always set to Lax for proxito cookies. + # Even if the donate app is present. + # Since we don't want to allow cookies from cross origin requests. + # This is django's default. + SESSION_COOKIE_SAMESITE = 'Lax' + @property def DATABASES(self): # This keeps connections to the DB alive,