diff --git a/dojo/user/ui/views.py b/dojo/user/ui/views.py index e5da6b4487c..cb1d9468150 100644 --- a/dojo/user/ui/views.py +++ b/dojo/user/ui/views.py @@ -11,6 +11,9 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm from django.contrib.auth.views import LoginView, PasswordResetConfirmView, PasswordResetView from django.contrib.humanize.templatetags.humanize import naturaltime +from django.contrib.sites.models import Site +from django.contrib.sites.requests import RequestSite +from django.contrib.sites.shortcuts import get_current_site from django.core import serializers from django.core.exceptions import PermissionDenied, ValidationError from django.core.mail import get_connection @@ -59,6 +62,27 @@ class DojoLoginView(LoginView): template_name = "dojo/login.html" authentication_form = AuthenticationForm + def get_context_data(self, **kwargs): + # Django's LoginView.get_context_data() resolves the current Site via + # get_current_site(), which raises Site.DoesNotExist (surfacing as an + # HTTP 500 on the login page) when the django_site row referenced by + # SITE_ID is absent on an instance. Resolve the site defensively and + # fall back to a request-derived site -- mirroring what Django itself + # does when the sites framework is not installed -- so the login page + # always renders instead of 500ing. + try: + current_site = get_current_site(self.request) + except Site.DoesNotExist: + current_site = RequestSite(self.request) + context = super(LoginView, self).get_context_data(**kwargs) + context.update({ + self.redirect_field_name: self.get_redirect_url(), + "site": current_site, + "site_name": current_site.name, + **(self.extra_context or {}), + }) + return context + def form_valid(self, form): last_login = None with contextlib.suppress(Exception): diff --git a/unittests/test_login_view_missing_site.py b/unittests/test_login_view_missing_site.py new file mode 100644 index 00000000000..e18ffd62ef0 --- /dev/null +++ b/unittests/test_login_view_missing_site.py @@ -0,0 +1,32 @@ +from django.contrib.sites.models import Site +from django.test import TestCase, override_settings + + +# Regression: GET /login returned HTTP 500 (Site.DoesNotExist) on instances +# whose django_site row referenced by SITE_ID was absent. Django's LoginView +# resolves the current Site while building the login page context, and raises +# when that row is missing; the login page must render regardless. +@override_settings(SECURE_SSL_REDIRECT=False) +class TestLoginViewMissingSite(TestCase): + + def test_login_page_renders_when_site_present(self): + # Control case: the default Site row exists, login renders normally. + self.assertTrue(Site.objects.exists()) + response = self.client.get("/login") + self.assertEqual( + response.status_code, 200, + msg=f"login GET returned {response.status_code} with a Site row present", + ) + + def test_login_page_renders_when_site_row_missing(self): + # Reproduce the reported condition: no django_site row matches SITE_ID. + Site.objects.all().delete() + self.assertFalse(Site.objects.exists()) + response = self.client.get("/login") + self.assertEqual( + response.status_code, 200, + msg=( + f"login GET returned {response.status_code} when the Site row is " + "missing (expected 200, not a Site.DoesNotExist 500)" + ), + )