diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 462b97caea6..4e7e314a25e 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -1,4 +1,5 @@ import structlog +from django.conf import settings from django.http import HttpResponse log = structlog.get_logger(__name__) @@ -33,3 +34,46 @@ def __call__(self, request): status=400, ) return self.get_response(request) + + +class UpdateCSPMiddleware: + """ + Middleware to update the CSP headers for specific views given its URL name. + + This is useful for views that we don't have much control over, + like views from third-party packages. For views that we have control over, + we should update the CSP headers directly in the view. + + Use the `RTD_CSP_UPDATE_HEADERS` setting to define the views that need to + update the CSP headers. The setting should be a dictionary where the key is + the URL name of the view and the value is a dictionary with the CSP headers, + for example: + + .. code-block:: python + + RTD_CSP_UPDATE_HEADERS = { + "login": {"form-action": ["https:"]}, + } + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + + # Views that raised an exception don't have a resolver_match object. + resolver_match = request.resolver_match + if not resolver_match: + return response + + url_name = resolver_match.url_name + update_csp_headers = settings.RTD_CSP_UPDATE_HEADERS + if settings.RTD_EXT_THEME_ENABLED and url_name in update_csp_headers: + if hasattr(response, "_csp_update"): + raise ValueError( + "Can't update CSP headers at the view and middleware at the same time, use one or the other." + ) + response._csp_update = update_csp_headers[url_name] + + return response diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index b05290526e4..1431137f11c 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -143,6 +143,7 @@ def SWITCH_PRODUCTION_DOMAIN(self): CSP_REPORT_URI = None CSP_REPORT_ONLY = False CSP_EXCLUDE_URL_PREFIXES = ("/admin/",) + RTD_CSP_UPDATE_HEADERS = {} # Read the Docs READ_THE_DOCS_EXTENSIONS = ext @@ -349,6 +350,7 @@ def MIDDLEWARE(self): "allauth.account.middleware.AccountMiddleware", "dj_pagination.middleware.PaginationMiddleware", "csp.middleware.CSPMiddleware", + "readthedocs.core.middleware.UpdateCSPMiddleware", "simple_history.middleware.HistoryRequestMiddleware", "readthedocs.core.logs.ReadTheDocsRequestMiddleware", "django_structlog.middlewares.CeleryMiddleware",