From 5cf95b500082d34b3f1de893cb4a72610b715327 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 20 Jun 2023 17:22:13 +0100 Subject: [PATCH 1/8] Allow Django 4.2 in setup.py and web.py --- omero/plugins/web.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/omero/plugins/web.py b/omero/plugins/web.py index 6edb2867d0..0d96b36976 100644 --- a/omero/plugins/web.py +++ b/omero/plugins/web.py @@ -76,7 +76,7 @@ def wrapper(self, *args, **kwargs): import django # NOQA except Exception: self.ctx.die(681, "ERROR: Django not installed!") - if django.VERSION < (3, 2) or django.VERSION >= (3, 3): + if django.VERSION < (3, 2) or django.VERSION >= (4, 3): self.ctx.err( "ERROR: Django version %s is not " "supported!" % django.get_version() diff --git a/setup.py b/setup.py index 6843dc50b7..ada163f704 100755 --- a/setup.py +++ b/setup.py @@ -54,8 +54,8 @@ def read(fname): "omero-py>=5.7.0", # minimum requirements for `omero web start` "concurrent-log-handler>=0.9.20", - "Django>=3.2.19,<4.0", - "django-pipeline==2.0.7", + "Django>=3.2.19,<4.3", + "django-pipeline==2.1.0", "django-cors-headers==3.7.0", "whitenoise>=5.3.0", "gunicorn>=19.3", From dd16238d7d74d2e431d3b13fd2bf9c5c8a60eede Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 20 Jun 2023 17:23:06 +0100 Subject: [PATCH 2/8] Add on_bind argument to monkey-patched django.core.servers.basehttp.run --- omeroweb/manage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/omeroweb/manage.py b/omeroweb/manage.py index 3e080e5f2c..1dd7228afd 100644 --- a/omeroweb/manage.py +++ b/omeroweb/manage.py @@ -60,10 +60,10 @@ # Monkeypatch Django development web server to always run in single thread # even if --nothreading is not specified on command line def force_nothreading( - addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer + addr, port, wsgi_handler, ipv6=False, threading=False, on_bind=None, server_cls=WSGIServer ): django_core_servers_basehttp_run( - addr, port, wsgi_handler, ipv6, False, server_cls + addr, port, wsgi_handler, ipv6, False, on_bind, server_cls ) import django.core.servers.basehttp From 1c88ffbc32aac19d0757b7e79dcae499c757a856 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 16:41:49 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- omeroweb/manage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/omeroweb/manage.py b/omeroweb/manage.py index 1dd7228afd..f3edf35590 100644 --- a/omeroweb/manage.py +++ b/omeroweb/manage.py @@ -60,7 +60,13 @@ # Monkeypatch Django development web server to always run in single thread # even if --nothreading is not specified on command line def force_nothreading( - addr, port, wsgi_handler, ipv6=False, threading=False, on_bind=None, server_cls=WSGIServer + addr, + port, + wsgi_handler, + ipv6=False, + threading=False, + on_bind=None, + server_cls=WSGIServer, ): django_core_servers_basehttp_run( addr, port, wsgi_handler, ipv6, False, on_bind, server_cls From 7b6ffe04afe92ad3eb882237b101728d430d1f32 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 21 Jun 2023 18:57:50 +0100 Subject: [PATCH 4/8] set os.environ['DJANGO_SETTINGS_MODULE'] early in web plugin --- omero/plugins/web.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/omero/plugins/web.py b/omero/plugins/web.py index 0d96b36976..2e7f7b4ed5 100644 --- a/omero/plugins/web.py +++ b/omero/plugins/web.py @@ -253,6 +253,11 @@ def _configure(self, parser): "Developer use: Loads the blitz gateway into a Python" " interpreter", ) + # THIS FIXES Django 4.2.x + os.environ["DJANGO_SETTINGS_MODULE"] = os.environ.get( + "DJANGO_SETTINGS_MODULE", "omeroweb.settings" + ) + @config_required def help(self, args, settings): """Return extended help""" From d1033f6a8f5d3a41c9bfb35f11fb97d63f1702bf Mon Sep 17 00:00:00 2001 From: Chris Allan Date: Mon, 3 Jul 2023 13:34:35 +0000 Subject: [PATCH 5/8] Revert "set os.environ['DJANGO_SETTINGS_MODULE'] early in web plugin" This reverts commit 7b6ffe04afe92ad3eb882237b101728d430d1f32. --- omero/plugins/web.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/omero/plugins/web.py b/omero/plugins/web.py index 2e7f7b4ed5..0d96b36976 100644 --- a/omero/plugins/web.py +++ b/omero/plugins/web.py @@ -253,11 +253,6 @@ def _configure(self, parser): "Developer use: Loads the blitz gateway into a Python" " interpreter", ) - # THIS FIXES Django 4.2.x - os.environ["DJANGO_SETTINGS_MODULE"] = os.environ.get( - "DJANGO_SETTINGS_MODULE", "omeroweb.settings" - ) - @config_required def help(self, args, settings): """Return extended help""" From aafe9f53455af91d02eaccf02a7f62316eae7894 Mon Sep 17 00:00:00 2001 From: Chris Allan Date: Mon, 3 Jul 2023 13:48:24 +0000 Subject: [PATCH 6/8] Port settings cleaning logic from Django 4.2.2 --- omeroweb/settings.py | 55 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/omeroweb/settings.py b/omeroweb/settings.py index be84ce31b3..6debc100f6 100755 --- a/omeroweb/settings.py +++ b/omeroweb/settings.py @@ -1408,10 +1408,55 @@ def process_custom_settings( LOGGING["loggers"][""]["level"] = "INFO" -def report_settings(module): - from django.views.debug import SafeExceptionReporterFilter +class CallableSettingWrapper: + """ + Object to wrap callable appearing in settings. + + Adapted from: + * `django.views.debug.CallableSettingWrapper()` + """ + + def __init__(self, callable_setting): + self._wrapped = callable_setting + + def __repr__(self): + return repr(self._wrapped) + + +def cleanse_setting(key, value): + """ + Cleanse an individual setting key/value of sensitive content. If the + value is a dictionary, recursively cleanse the keys in that dictionary. - setting_filter = SafeExceptionReporterFilter() + Adapted from: + * `django.views.debug.SafeExceptionReporterFilter.cleanse_setting()` + """ + cleansed_substitute = "********************" + hidden_settings = re.compile("API|TOKEN|KEY|SECRET|PASS|SIGNATUREE", flags=re.I) + + try: + is_sensitive = hidden_settings.search(key) + except TypeError: + is_sensitive = False + + if is_sensitive: + cleansed = cleansed_substitute + elif isinstance(value, dict): + cleansed = {k: cleanse_setting(k, v) for k, v in value.items()} + elif isinstance(value, list): + cleansed = [cleanse_setting("", v) for v in value] + elif isinstance(value, tuple): + cleansed = tuple([cleanse_setting("", v) for v in value]) + else: + cleansed = value + + if callable(cleansed): + cleansed = CallableSettingWrapper(cleansed) + + return cleansed + + +def report_settings(module): custom_settings_mappings = getattr(module, "CUSTOM_SETTINGS_MAPPINGS", {}) for key in sorted(custom_settings_mappings): values = custom_settings_mappings[key] @@ -1422,7 +1467,7 @@ def report_settings(module): logger.debug( "%s = %r (source:%s)", global_name, - setting_filter.cleanse_setting(global_name, global_value), + cleanse_setting(global_name, global_value), source, ) @@ -1435,7 +1480,7 @@ def report_settings(module): logger.debug( "%s = %r (deprecated:%s, %s)", global_name, - setting_filter.cleanse_setting(global_name, global_value), + cleanse_setting(global_name, global_value), key, description, ) From e8a7347dbc1314e7132f46de12ac5f410b550f57 Mon Sep 17 00:00:00 2001 From: Chris Allan Date: Mon, 3 Jul 2023 13:56:31 +0000 Subject: [PATCH 7/8] Update django-redis to 5.3.0 for better Django 4.2 compatibility --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ada163f704..20c5c5aca1 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,6 @@ def read(fname): include_package_data=True, tests_require=["pytest"], extras_require={ - "redis": ["django-redis==5.0.0"], + "redis": ["django-redis==5.3.0"], }, ) From 77abef8b517944594de523eb83715f345019c6da Mon Sep 17 00:00:00 2001 From: Chris Allan Date: Mon, 3 Jul 2023 14:18:37 +0000 Subject: [PATCH 8/8] Remove our `GzipMiddleware` which doesn't defend against BREACH This middleware is not in use anywhere in the codebase, has not had substantial updates for 10+ years and has long been superceded by: * https://docs.djangoproject.com/en/4.2/ref/middleware/#module-django.middleware.gzip See also: * https://docs.djangoproject.com/en/4.2/releases/4.2/#mitigation-for-the-breach-attack --- omeroweb/webgateway/middleware.py | 55 ------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 omeroweb/webgateway/middleware.py diff --git a/omeroweb/webgateway/middleware.py b/omeroweb/webgateway/middleware.py deleted file mode 100644 index b24f537609..0000000000 --- a/omeroweb/webgateway/middleware.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import re - -from django.utils.text import compress_string -from django.utils.cache import patch_vary_headers - -re_accepts_gzip = re.compile(r"\bgzip\b") - - -class GZipMiddleware(object): - """ - This middleware compresses content if the browser allows gzip compression. - It sets the Vary header accordingly, so that caches will base their storage - on the Accept-Encoding header. - """ - - def process_response(self, request, response): - # It's not worth compressing non-OK or really short responses. - # omeroweb: the tradeoff for less than 8192k of uncompressed text is - # not worth it most of the times. - if response.status_code != 200 or len(response.content) < 8192: - return response - - # Avoid gzipping if we've already got a content-encoding. - if response.has_header("Content-Encoding"): - return response - - # omeroweb: we don't want to compress everything, so doing an opt-in - # approach - ctype = response.get("Content-Type", "").lower() - if "javascript" not in ctype and "text" not in ctype: - return response - - patch_vary_headers(response, ("Accept-Encoding",)) - - # Avoid gzipping if we've already got a content-encoding. - if response.has_header("Content-Encoding"): - return response - - # Older versions of IE have issues with gzipped pages containing either - # Javascript and PDF. - if "msie" in request.META.get("HTTP_USER_AGENT", "").lower(): - ctype = response.get("Content-Type", "").lower() - if "javascript" in ctype or ctype == "application/pdf": - return response - - ae = request.META.get("HTTP_ACCEPT_ENCODING", "") - if not re_accepts_gzip.search(ae): - return response - - response.content = compress_string(response.content) - response["Content-Encoding"] = "gzip" - response["Content-Length"] = str(len(response.content)) - return response