From 41c4bd00b6e09d9c0992598c6cca3e38201721b7 Mon Sep 17 00:00:00 2001 From: James Meakin <12661555+jmsmkn@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:24:45 +0100 Subject: [PATCH] Add application for `.well-known` urls Add security.txt on all domains Closes #2875 --- app/config/settings.py | 1 + app/config/urls/challenge_subdomain.py | 7 +---- app/config/urls/rendering_subdomain.py | 8 ++---- app/config/urls/root.py | 11 +++----- app/grandchallenge/well_known/__init__.py | 0 .../templates/well_known}/robots.txt | 0 .../templates/well_known/security.txt | 5 ++++ app/grandchallenge/well_known/urls.py | 17 +++++++++++ app/grandchallenge/well_known/views.py | 14 ++++++++++ app/tests/core_tests/integration_tests.py | 2 +- app/tests/well_known_tests/__init__.py | 0 app/tests/well_known_tests/test_views.py | 28 +++++++++++++++++++ dockerfiles/http/nginx.conf.template | 6 ---- 13 files changed, 73 insertions(+), 26 deletions(-) create mode 100644 app/grandchallenge/well_known/__init__.py rename app/grandchallenge/{core/templates => well_known/templates/well_known}/robots.txt (100%) create mode 100644 app/grandchallenge/well_known/templates/well_known/security.txt create mode 100644 app/grandchallenge/well_known/urls.py create mode 100644 app/grandchallenge/well_known/views.py create mode 100644 app/tests/well_known_tests/__init__.py create mode 100644 app/tests/well_known_tests/test_views.py diff --git a/app/config/settings.py b/app/config/settings.py index 2450904b29..4e341bdb6e 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -609,6 +609,7 @@ def get_private_ip(): "grandchallenge.direct_messages", "grandchallenge.incentives", "grandchallenge.browser_sessions", + "grandchallenge.well_known", ] INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS + THIRD_PARTY_APPS diff --git a/app/config/urls/challenge_subdomain.py b/app/config/urls/challenge_subdomain.py index ca9e3bec21..71dfc9b925 100644 --- a/app/config/urls/challenge_subdomain.py +++ b/app/config/urls/challenge_subdomain.py @@ -1,6 +1,5 @@ from django.conf import settings from django.urls import include, path -from django.views.generic import TemplateView from grandchallenge.challenges.views import ( ChallengeUpdate, @@ -13,11 +12,7 @@ urlpatterns = [ path( - "robots.txt", - TemplateView.as_view( - template_name="robots.txt", content_type="text/plain" - ), - name="subdomain_robots_txt", + "", include("grandchallenge.well_known.urls", namespace="well-known") ), path( "evaluation/", diff --git a/app/config/urls/rendering_subdomain.py b/app/config/urls/rendering_subdomain.py index 98b5bc89ea..ee257f86b5 100644 --- a/app/config/urls/rendering_subdomain.py +++ b/app/config/urls/rendering_subdomain.py @@ -1,6 +1,5 @@ from django.conf import settings -from django.urls import path -from django.views.generic import TemplateView +from django.urls import include, path from grandchallenge.core.views import healthcheck from grandchallenge.serving.views import serve_images @@ -11,10 +10,7 @@ urlpatterns = [ path( - "robots.txt", - TemplateView.as_view( - template_name="robots.txt", content_type="text/plain" - ), + "", include("grandchallenge.well_known.urls", namespace="well-known") ), path( "healthcheck/", diff --git a/app/config/urls/root.py b/app/config/urls/root.py index 2e8e4feb5c..df8fcdcc65 100644 --- a/app/config/urls/root.py +++ b/app/config/urls/root.py @@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.sitemaps.views import sitemap from django.urls import include, path -from django.views.generic import RedirectView, TemplateView +from django.views.generic import RedirectView from machina import urls as machina_urls from grandchallenge.algorithms.sitemaps import AlgorithmsSitemap @@ -41,18 +41,15 @@ } urlpatterns = [ + path( + "", include("grandchallenge.well_known.urls", namespace="well-known") + ), path("", HomeTemplate.as_view(), name="home"), path( "challenge-suspended/", ChallengeSuspendedView.as_view(), name="challenge-suspended", ), - path( - "robots.txt", - TemplateView.as_view( - template_name="robots.txt", content_type="text/plain" - ), - ), path( "sitemap.xml", sitemap, diff --git a/app/grandchallenge/well_known/__init__.py b/app/grandchallenge/well_known/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/grandchallenge/core/templates/robots.txt b/app/grandchallenge/well_known/templates/well_known/robots.txt similarity index 100% rename from app/grandchallenge/core/templates/robots.txt rename to app/grandchallenge/well_known/templates/well_known/robots.txt diff --git a/app/grandchallenge/well_known/templates/well_known/security.txt b/app/grandchallenge/well_known/templates/well_known/security.txt new file mode 100644 index 0000000000..3ee37d8125 --- /dev/null +++ b/app/grandchallenge/well_known/templates/well_known/security.txt @@ -0,0 +1,5 @@ +Contact: mailto:security@{{ current_site.domain }} +Contact: mailto:support@{{ current_site.domain }} +Canonical: https://{{ current_site.domain }}/.well-known/security.txt +Preferred-Languages: en, nl +Expires: 2026-02-16T23:59:59z diff --git a/app/grandchallenge/well_known/urls.py b/app/grandchallenge/well_known/urls.py new file mode 100644 index 0000000000..2b85040ef7 --- /dev/null +++ b/app/grandchallenge/well_known/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from django.views.generic import TemplateView + +from grandchallenge.well_known.views import SecurityTXT + +app_name = "well_known" + +urlpatterns = [ + path( + "robots.txt", + TemplateView.as_view( + template_name="well_known/robots.txt", content_type="text/plain" + ), + name="robots_txt", + ), + path(".well-known/security.txt", SecurityTXT.as_view()), +] diff --git a/app/grandchallenge/well_known/views.py b/app/grandchallenge/well_known/views.py new file mode 100644 index 0000000000..ce7126add7 --- /dev/null +++ b/app/grandchallenge/well_known/views.py @@ -0,0 +1,14 @@ +from django.contrib.sites.models import Site +from django.views.generic import TemplateView + + +class SecurityTXT(TemplateView): + template_name = "well_known/security.txt" + content_type = "text/plain" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["current_site"] = Site.objects.get_current( + request=self.request + ) + return context diff --git a/app/tests/core_tests/integration_tests.py b/app/tests/core_tests/integration_tests.py index 7e5ae2e2ae..a941a02084 100644 --- a/app/tests/core_tests/integration_tests.py +++ b/app/tests/core_tests/integration_tests.py @@ -376,7 +376,7 @@ def test_robots_txt_can_be_loaded(self): # main domain robots.txt robots_url = "/robots.txt" robots_url_project = reverse( - "subdomain_robots_txt", + "well_known:robots_txt", kwargs={"challenge_short_name": self.testchallenge.short_name}, ) self._test_url_can_be_viewed(None, robots_url) # None = not logged in diff --git a/app/tests/well_known_tests/__init__.py b/app/tests/well_known_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/tests/well_known_tests/test_views.py b/app/tests/well_known_tests/test_views.py new file mode 100644 index 0000000000..849994323d --- /dev/null +++ b/app/tests/well_known_tests/test_views.py @@ -0,0 +1,28 @@ +from datetime import timedelta + +import pytest +from dateutil.parser import isoparse +from django.utils.timezone import now + + +@pytest.mark.django_db +def test_security_txt_expiry_valid(client): + # If this test fails be sure to review the security.txt + # and update the expiry date + response = client.get("/.well-known/security.txt") + + assert response.status_code == 200 + + lines = response.rendered_content.splitlines() + + # Last line should contain the expiry clause + key, value = lines[-1].split(":", 1) + + assert key == "Expires" + + expiry_date = isoparse(value.strip()) + expires_in = expiry_date - now() + + # Must be more than a month and less than a year + # See https://www.rfc-editor.org/rfc/rfc9116#name-expires + assert timedelta(days=28) < expires_in < timedelta(days=365) diff --git a/dockerfiles/http/nginx.conf.template b/dockerfiles/http/nginx.conf.template index 5435a9f2aa..c973daddbd 100644 --- a/dockerfiles/http/nginx.conf.template +++ b/dockerfiles/http/nginx.conf.template @@ -117,12 +117,6 @@ http { location / { rewrite ^ https://$host$request_uri? permanent; } - - # for certbot challenges - location ~ /.well-known/acme-challenge { - allow all; - root /data/letsencrypt; - } } server {