From 2cbf7b25c44e2b44bbc72a58f799f5026471f9da Mon Sep 17 00:00:00 2001 From: Antonis Christofides Date: Tue, 9 Jan 2024 18:38:00 +0200 Subject: [PATCH] Add option to require authentication --- doc/general/install.rst | 26 +++++++++- doc/general/release-notes.rst | 3 ++ enhydris/api/tests/test_auth.py | 1 + .../api/tests/test_views/test_bounding_box.py | 3 ++ .../api/tests/test_views/test_gentity_file.py | 4 ++ enhydris/api/tests/test_views/test_misc.py | 8 ++++ enhydris/api/tests/test_views/test_search.py | 3 +- enhydris/api/tests/test_views/test_station.py | 6 +++ .../api/tests/test_views/test_station_sort.py | 4 ++ .../api/tests/test_views/test_timeseries.py | 20 ++++++++ .../tests/test_views/test_timeseries_group.py | 2 + enhydris/middleware.py | 47 +++++++++++++++++++ .../test_views/test_telemetry_wizard_view.py | 10 ++++ enhydris/tests/test_forms/test_auth.py | 1 + enhydris/tests/test_middleware.py | 36 ++++++++++++++ enhydris/tests/test_views/test_auth.py | 1 + enhydris/tests/test_views/test_station.py | 12 +++++ enhydris/tests/test_views/test_timeseries.py | 4 +- .../tests/test_views/test_timeseries_group.py | 4 +- enhydris_project/settings/__init__.py | 2 + 20 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 enhydris/middleware.py create mode 100644 enhydris/tests/test_middleware.py diff --git a/doc/general/install.rst b/doc/general/install.rst index d67ff0d5..b95d1473 100644 --- a/doc/general/install.rst +++ b/doc/general/install.rst @@ -236,6 +236,22 @@ These are the settings available to Enhydris, in addition to the .. _django settings: http://docs.djangoproject.com/en/3.2/ref/settings/ +Authentication settings +----------------------- + +.. data:: ENHYDRIS_REQUIRE_AUTHENTICATION + + If ``True``, users must be logged on to do anything, such as view a + list of stations. All API views except for login will return 401, and + all non-API views except for login will redirect to the login page. + In that case, :attr:`enhydris.models.Timeseries.publicly_available` + and :data:`ENHYDRIS_DEFAULT_PUBLICLY_AVAILABLE` will obviously not + have any effect. :data:`REGISTRATION_OPEN` will also not work + (because the registration page will also redirect to the login page), + but it should be kept at ``False``. + + The default for ``ENHYDRIS_REQUIRE_AUTHENTICATION`` is ``False``. + .. data:: REGISTRATION_OPEN If ``True``, users can register, otherwise they have to be created @@ -270,7 +286,7 @@ These are the settings available to Enhydris, in addition to the If this is ``False`` (the default), all logged on users have permission to download the time series data for all time series (for - anonymous user there's a + anonymous users there's a :attr:`enhydris.models.Timeseries.publicly_available` attribute for each individual time series; see also :data:`ENHYDRIS_DEFAULT_PUBLICLY_AVAILABLE`). Note that if you want @@ -286,6 +302,9 @@ These are the settings available to Enhydris, in addition to the applies to all time series of a station. Individual time series can again be marked as publicly available. +Map settings +------------ + .. data:: ENHYDRIS_MAP_BASE_LAYERS A dictionary of JavaScript definitions of base layers to use on the map. @@ -317,7 +336,7 @@ These are the settings available to Enhydris, in addition to the .. data:: ENHYDRIS_MAP_DEFAULT_BASE_LAYER The name of the base layer that is visible by default; it must be a key in - data:`ENHYDRIS_MAP_BASE_LAYERS`. The default is "Open Street Map". + :data:`ENHYDRIS_MAP_BASE_LAYERS`. The default is "Open Street Map". .. data:: ENHYDRIS_MAP_MIN_VIEWPORT_SIZE @@ -336,6 +355,9 @@ These are the settings available to Enhydris, in addition to the lat is in decimal degrees, positive for north/east, negative for west/south. +Miscellaneous settings +---------------------- + .. data:: ENHYDRIS_SITES_FOR_NEW_STATIONS A set of site (i.e. domain) ids of the Django sites framework. The diff --git a/doc/general/release-notes.rst b/doc/general/release-notes.rst index ea810d9d..b42474d3 100644 --- a/doc/general/release-notes.rst +++ b/doc/general/release-notes.rst @@ -113,6 +113,9 @@ Another important difference is that these new settings apply only to time series data, not to gentity files, so gentity files are now always publicly available. +In addition, setting :data:`ENHYDRIS_AUTHENTICATION_REQUIRED` has been +added and can make Enhydris fully closed. + Version 3.0 =========== diff --git a/enhydris/api/tests/test_auth.py b/enhydris/api/tests/test_auth.py index cbd8d177..284c47a1 100644 --- a/enhydris/api/tests/test_auth.py +++ b/enhydris/api/tests/test_auth.py @@ -16,6 +16,7 @@ from rest_framework.test import APITestCase +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class AuthTestCase(APITestCase): def setUp(self): User = get_user_model() diff --git a/enhydris/api/tests/test_views/test_bounding_box.py b/enhydris/api/tests/test_views/test_bounding_box.py index aa02f3b5..e58aa8a8 100644 --- a/enhydris/api/tests/test_views/test_bounding_box.py +++ b/enhydris/api/tests/test_views/test_bounding_box.py @@ -7,6 +7,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class SimpleBoundingBoxTestCase(APITestCase): def setUp(self): mommy.make( @@ -36,6 +37,7 @@ def test_y2(self): @override_settings(ENHYDRIS_MAP_DEFAULT_VIEWPORT=(1.0, 2.0, 3.0, 4.0)) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class DefaultBoundingBoxTestCase(APITestCase): def setUp(self): response = self.client.get("/api/stations/") @@ -55,6 +57,7 @@ def test_y2(self): @override_settings(ENHYDRIS_MAP_MIN_VIEWPORT_SIZE=1.0) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TooSmallBoundingBoxTestCase(APITestCase): def setUp(self): mommy.make( diff --git a/enhydris/api/tests/test_views/test_gentity_file.py b/enhydris/api/tests/test_views/test_gentity_file.py index 7e3c0571..82cff985 100644 --- a/enhydris/api/tests/test_views/test_gentity_file.py +++ b/enhydris/api/tests/test_views/test_gentity_file.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, mock_open, patch from django.db.models.fields.files import FieldFile +from django.test import override_settings from rest_framework.test import APITestCase from model_mommy import mommy @@ -8,6 +9,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GentityFileTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station) @@ -48,6 +50,7 @@ def test_detail_returns_nothing_if_wrong_station(self): self.assertEqual(r.status_code, 404) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GentityFileContentTestCase(APITestCase): def setUp(self): # Mocking. We mock several things in Django and Python so that: @@ -82,6 +85,7 @@ def test_content_type(self): self.assertEqual(self.response["Content-Type"], "image/jpeg") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GentityFileContentWithoutFileTestCase(APITestCase): def setUp(self): # Mommy creates a GentityFile without an associated file, so the diff --git a/enhydris/api/tests/test_views/test_misc.py b/enhydris/api/tests/test_views/test_misc.py index 83ec4db2..e7e31b87 100644 --- a/enhydris/api/tests/test_views/test_misc.py +++ b/enhydris/api/tests/test_views/test_misc.py @@ -1,3 +1,4 @@ +from django.test import override_settings from rest_framework.test import APITestCase from model_mommy import mommy @@ -5,6 +6,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GareaTestCase(APITestCase): def setUp(self): self.garea = mommy.make(models.Garea) @@ -14,6 +16,7 @@ def test_get_garea(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class OrganizationTestCase(APITestCase): def setUp(self): self.organization = mommy.make(models.Organization) @@ -23,6 +26,7 @@ def test_get_organization(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class PersonTestCase(APITestCase): def setUp(self): self.person = mommy.make(models.Person) @@ -32,6 +36,7 @@ def test_get_person(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class EventTypeTestCase(APITestCase): def setUp(self): self.event_type = mommy.make(models.EventType) @@ -41,6 +46,7 @@ def test_get_event_type(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class VariableTestCase(APITestCase): def setUp(self): self.variable = mommy.make(models.Variable, descr="Temperature") @@ -50,6 +56,7 @@ def test_get_variable(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class UnitOfMeasurementTestCase(APITestCase): def setUp(self): self.unit_of_measurement = mommy.make(models.UnitOfMeasurement) @@ -59,6 +66,7 @@ def test_get_unit_of_measurement(self): self.assertEqual(r.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GentityEventTestCase(APITestCase): # We have extensively tested GentityFile, which is practically the same code, # so we test this briefly. diff --git a/enhydris/api/tests/test_views/test_search.py b/enhydris/api/tests/test_views/test_search.py index 7fa73375..99379f4d 100644 --- a/enhydris/api/tests/test_views/test_search.py +++ b/enhydris/api/tests/test_views/test_search.py @@ -29,7 +29,8 @@ def _create_models(self): def setUp(self): self._create_models() - self.response = self.client.get("/api/stations/", {"q": self.search_term}) + with override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False): + self.response = self.client.get("/api/stations/", {"q": self.search_term}) @abstractmethod def _create_models(self): diff --git a/enhydris/api/tests/test_views/test_station.py b/enhydris/api/tests/test_views/test_station.py index 43f97266..9ded844c 100644 --- a/enhydris/api/tests/test_views/test_station.py +++ b/enhydris/api/tests/test_views/test_station.py @@ -12,6 +12,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationListTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station, name="Hobbiton") @@ -28,6 +29,7 @@ def test_name(self): @override_settings(SITE_ID=1) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationListSitesTestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -47,6 +49,7 @@ def test_list_does_not_contain_arta(self): @override_settings(SITE_ID=1) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationDetailSitesTestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -64,6 +67,7 @@ def test_hobbiton_detail_unavailable_on_site2(self): self.assertEquals(response.status_code, 404) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationCreateTestCase(APITestCase): def setUp(self): self.user = mommy.make(User, is_active=True, is_superuser=False) @@ -111,6 +115,7 @@ def test_any_user_can_create_station_when_system_is_open(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationUpdateAndDeleteTestCase(APITestCase): def setUp(self): self.user1 = mommy.make(User, is_active=True, is_superuser=False) @@ -181,6 +186,7 @@ def test_authorized_user_can_delete_station(self): self.assertEqual(response.status_code, 204, response.content) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationCsvTestCase(APITestCase): def setUp(self): self._create_stations() diff --git a/enhydris/api/tests/test_views/test_station_sort.py b/enhydris/api/tests/test_views/test_station_sort.py index 0d3d592c..bb2796cb 100644 --- a/enhydris/api/tests/test_views/test_station_sort.py +++ b/enhydris/api/tests/test_views/test_station_sort.py @@ -1,5 +1,6 @@ from unittest.mock import patch +from django.test import override_settings from rest_framework.test import APITestCase from model_mommy import mommy @@ -7,6 +8,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationSortDefaultTestCase(APITestCase): def setUp(self): mommy.make(models.Station, name="Rivendell") @@ -23,6 +25,7 @@ def test_hobbiton_is_first(self): self.assertEqual(self.response.json()["results"][0]["name"], "Hobbiton") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationSortByReverseNameTestCase(APITestCase): def setUp(self): mommy.make(models.Station, name="Rivendell") @@ -39,6 +42,7 @@ def test_rivendell_is_first(self): self.assertEqual(self.response.json()["results"][0]["name"], "Rivendell") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) @patch( "django.db.models.query.QuerySet.order_by", return_value=models.Station.objects.none(), diff --git a/enhydris/api/tests/test_views/test_timeseries.py b/enhydris/api/tests/test_views/test_timeseries.py index b0a70256..beb96d54 100644 --- a/enhydris/api/tests/test_views/test_timeseries.py +++ b/enhydris/api/tests/test_views/test_timeseries.py @@ -18,6 +18,7 @@ from enhydris.tests import TimeseriesDataMixin +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class Tsdata404TestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station) @@ -37,6 +38,7 @@ def test_post_nonexistent_timeseries(self): @patch("enhydris.models.Timeseries.get_data", return_value=HTimeseries()) @override_settings(ENHYDRIS_ENABLE_TIMESERIES_DATA_VIEWERS=False) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataGetPermissionsTestCase(APITestCase): @classmethod def setUpTestData(cls): @@ -73,6 +75,7 @@ def test_logged_on_user_is_ok(self, m): self.assertEqual(self.response.status_code, 200) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GetDataTestCase(APITestCase, TimeseriesDataMixin): @classmethod def setUpTestData(cls): @@ -108,6 +111,7 @@ def test_response_content_in_other_timezone(self): ) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GetDataInVariousFormatsTestCase(APITestCase, TimeseriesDataMixin): def setUp(self): super().setUp() @@ -181,6 +185,7 @@ def test_response_headers_csv_default(self): ) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataPostTestCase(APITestCase): @patch("enhydris.models.Timeseries.append_data") def setUp(self, m): @@ -222,6 +227,7 @@ def test_called_append_data_with_correct_timezone(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataPostAuthorizationTestCase(APITestCase): def setUp(self): self.user1 = mommy.make(User, is_active=True, is_superuser=False) @@ -261,6 +267,7 @@ def test_authorized_user_can_posttsdata(self, m): self.assertEqual(self._post_tsdata().status_code, 204) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataPostGarbageTestCase(APITestCase): @patch("enhydris.models.Timeseries.append_data", side_effect=iso8601.ParseError) def setUp(self, m): @@ -289,6 +296,7 @@ def test_status_code(self): self.assertEqual(self.response.status_code, 400) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataPostDuplicateTimestampsTestCase(APITestCase): def setUp(self): user = mommy.make(User, username="admin", is_superuser=True) @@ -315,6 +323,7 @@ def test_status_code(self): ) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataStartAndEndDateTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station) @@ -371,6 +380,7 @@ def test_called_get_data_with_very_late_start_date(self, m): ) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataInvalidStartOrEndDateTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station) @@ -401,6 +411,7 @@ def test_invalid_end_date(self, m): m.assert_called_once_with(start_date=None, end_date=None, timezone=None) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TsdataHeadTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station) @@ -434,6 +445,7 @@ def test_head(self): self.assertNotContains(response, "2018-12-09 13:10,") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesBottomTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station, display_timezone="Etc/GMT-2") @@ -479,6 +491,7 @@ def test_response_content_with_timezone(self): @override_settings(ENHYDRIS_ENABLE_TIMESERIES_DATA_VIEWERS=False) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesBottomPermissionsTestCase(APITestCase): def setUp(self): station = mommy.make(models.Station) @@ -512,6 +525,7 @@ def test_logged_on_user_is_ok(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesPostTestCase(APITestCase): def setUp(self): self.user1 = mommy.make(User, is_active=True, is_superuser=False) @@ -558,6 +572,7 @@ def test_returns_proper_error_when_creating_second_initial_timeseries(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesPostWithWrongStationOrTimeseriesGroupTestCase(APITestCase): def setUp(self): self.user = mommy.make(User, is_active=True, is_superuser=False) @@ -617,6 +632,7 @@ def test_create_timeseries_with_everything_correct(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesPostWithWrongTimeseriesTypeTestCase(APITestCase): def setUp(self): self.user = mommy.make(User, is_active=True, is_superuser=False) @@ -651,6 +667,7 @@ def test_create_timeseries_with_correct_type(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesDeleteTestCase(APITestCase): def setUp(self): self.user1 = mommy.make(User, is_active=True, is_superuser=False) @@ -684,6 +701,7 @@ def test_authorized_user_can_delete_timeseries(self): self.assertEqual(response.status_code, 204) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) @patch("enhydris.api.views.TimeseriesViewSet._get_stats_for_all_intervals") @patch("enhydris.models.Timeseries.get_data") class TimeseriesChartDateBoundsTestCase(APITestCase, TimeseriesDataMixin): @@ -756,6 +774,7 @@ def _value(self, yyyymmddhhmm, min, max, mean): } +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) @patch("enhydris.api.views.TimeseriesViewSet.CHART_MAX_INTERVALS", new=20) @patch("enhydris.models.Timeseries.get_data") @override_settings(ENHYDRIS_ENABLE_TIMESERIES_DATA_VIEWERS=False) @@ -815,6 +834,7 @@ def test_null_values_are_dropped(self, mock): self._assertChartResponse(response, expected) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) @patch("enhydris.api.views.TimeseriesViewSet.CHART_MAX_INTERVALS", new=3) @patch("enhydris.models.Timeseries.get_data") class TimeseriesChartValuesTestCase( diff --git a/enhydris/api/tests/test_views/test_timeseries_group.py b/enhydris/api/tests/test_views/test_timeseries_group.py index c2a6df4d..6a7eff05 100644 --- a/enhydris/api/tests/test_views/test_timeseries_group.py +++ b/enhydris/api/tests/test_views/test_timeseries_group.py @@ -7,6 +7,7 @@ from enhydris import models +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesGroupListTestCase(APITestCase): def setUp(self): self.station = mommy.make(models.Station, name="Hobbiton") @@ -28,6 +29,7 @@ def test_name(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesGroupPostTestCase(APITestCase): def setUp(self): self.user1 = mommy.make(User, is_active=True, is_superuser=False) diff --git a/enhydris/middleware.py b/enhydris/middleware.py new file mode 100644 index 00000000..f93cf23c --- /dev/null +++ b/enhydris/middleware.py @@ -0,0 +1,47 @@ +from urllib.parse import urlencode + +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import redirect + + +class AuthenticationRequired(Exception): + pass + + +class GlobalLoginRequiredMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + try: + self._check_for_user_login(request) + except AuthenticationRequired: + return self._authentication_required_response(request) + + return self.get_response(request) + + def _check_for_user_login(self, request): + auth_required_setting = settings.ENHYDRIS_AUTHENTICATION_REQUIRED + if not auth_required_setting or request.user.is_authenticated: + return + self._ensure_view_is_allowed_unauthenticated(request) + + def _ensure_view_is_allowed_unauthenticated(self, request): + allowed_views = [ + settings.LOGIN_URL, + "/accounts/password/reset/", + "/api/auth/login/", + "/api/auth/password/reset/", + ] + is_allowed_view = any([request.path.startswith(x) for x in allowed_views]) + if not is_allowed_view: + raise AuthenticationRequired() + + def _authentication_required_response(self, request): + if request.path.startswith("/api/"): + return HttpResponse("Authentication required", status=401) + else: + next_url = request.get_full_path() + redirect_url = f"{settings.LOGIN_URL}?{urlencode({'next': next_url})}" + return redirect(redirect_url) diff --git a/enhydris/telemetry/tests/test_views/test_telemetry_wizard_view.py b/enhydris/telemetry/tests/test_views/test_telemetry_wizard_view.py index ba16efca..21359a07 100644 --- a/enhydris/telemetry/tests/test_views/test_telemetry_wizard_view.py +++ b/enhydris/telemetry/tests/test_views/test_telemetry_wizard_view.py @@ -15,6 +15,7 @@ @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class RedirectToFirstStepTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -45,6 +46,7 @@ def test_no_redirects(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class CopyTelemetryDataFromDatabaseToSessionTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -80,6 +82,7 @@ def test_default_fetch_offset_minutes_is_empty(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class CopyTelemetryDataFromDatabaseToSessionWithExistingTelemetryTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -143,6 +146,7 @@ def test_configuration_is_in_the_session(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class FirstStepPostTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -180,6 +184,7 @@ def test_fetch_offset_minutes(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class FirstStepPostErrorTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -265,6 +270,7 @@ def test_form_created_with_the_correct_configuration(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class SecondStepGetWithNondefaultConfigurationTestCase(SecondStepGetMixin, TestCase): # This is essentially the same as # SecondStepGetTestCase.test_form_created_with_the_correct_configuration, except @@ -331,6 +337,7 @@ def json(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class SecondStepPostWithErrorTestCase(SecondStepPostMixin, TestCase): @classmethod def _post_step_2(cls): @@ -415,6 +422,7 @@ def tearDownClass(cls): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class FinalStepPostSuccessfulTestCase(FinalStepPostSuccessfulMixin, TestCase): def test_saves_stuff_in_database(self): telemetry = Telemetry.objects.get(station=self.station) @@ -453,6 +461,7 @@ def test_replaces_stuff_in_database(self): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) @patch( "enhydris.telemetry.types.meteoview2.requests.request", return_value=MockResponse( @@ -506,6 +515,7 @@ def test_button_says_finish_in_final_step(self, m): @override_settings(ENHYDRIS_USERS_CAN_ADD_CONTENT=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class PermissionsTestCase(TestCase): @classmethod def setUpTestData(cls): diff --git a/enhydris/tests/test_forms/test_auth.py b/enhydris/tests/test_forms/test_auth.py index 5b9b32ba..4114248b 100644 --- a/enhydris/tests/test_forms/test_auth.py +++ b/enhydris/tests/test_forms/test_auth.py @@ -4,6 +4,7 @@ @override_settings(REGISTRATION_OPEN=True) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class RegistrationFormTestCase(TestCase): def test_registration_form_submission(self): post_data = {"usename": "bob", "password": "topsecret"} diff --git a/enhydris/tests/test_middleware.py b/enhydris/tests/test_middleware.py new file mode 100644 index 00000000..bfc8350c --- /dev/null +++ b/enhydris/tests/test_middleware.py @@ -0,0 +1,36 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase, override_settings + + +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=True) +class AuthenticationRequiredTestCase(TestCase): + @classmethod + def setUpTestData(cls): + User.objects.create_user("alice", password="topsecret", is_superuser=True) + + def test_simple_view(self): + response = self.client.get("/nonexistent/") + self.assertRedirects(response, f"{settings.LOGIN_URL}?next=/nonexistent/") + + def test_password_reset(self): + response = self.client.get("/accounts/password/reset/") + self.assertEqual(response.status_code, 200) + + def test_api_view(self): + response = self.client.get("/api/nonexistent/") + self.assertEqual(response.status_code, 401) + + def test_api_view_with_login(self): + login_successful = self.client.login(username="alice", password="topsecret") + assert login_successful + response = self.client.get("/nonexistent/") + self.assertEqual(response.status_code, 404) + + def test_login_api_view(self): + response = self.client.options("/api/auth/login/") + self.assertEqual(response.status_code, 200) + + def test_api_password_reset(self): + response = self.client.options("/api/auth/password/reset/") + self.assertEqual(response.status_code, 200) diff --git a/enhydris/tests/test_views/test_auth.py b/enhydris/tests/test_views/test_auth.py index 23800b53..71de6d95 100644 --- a/enhydris/tests/test_views/test_auth.py +++ b/enhydris/tests/test_views/test_auth.py @@ -6,6 +6,7 @@ # NOTE: For an explanation of how captchas work in the tests, see CAPTCHA_TEST_MODE # in settings.py. +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class RegisterTestCase(TransactionTestCase): available_apps = [ "django.contrib.auth", diff --git a/enhydris/tests/test_views/test_station.py b/enhydris/tests/test_views/test_station.py index f84ad5d8..4717c285 100644 --- a/enhydris/tests/test_views/test_station.py +++ b/enhydris/tests/test_views/test_station.py @@ -17,6 +17,7 @@ from enhydris.tests import SeleniumTestCase, TimeseriesDataMixin +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationListTestCase(TestCase): @staticmethod def _create_station(name, x, y, srid=4326, original_srid=4326): @@ -81,6 +82,7 @@ def test_default_map_viewport_when_given_as_a_tuple(self): @override_settings(SITE_ID=1) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationListSitesTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -100,6 +102,7 @@ def test_list_does_not_contain_komboti(self): self.assertNotContains(response, "Komboti") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationDetailTestCase(TestCase, TimeseriesDataMixin): def setUp(self): self.create_timeseries() @@ -119,6 +122,7 @@ def test_timeseries_group_list(self): @override_settings(MEDIA_URL="/media/") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationDetailImagesTestCase(TestCase): def setUp(self): self.station = mommy.make(Station) @@ -154,6 +158,7 @@ def test_first_non_featured_image_when_one_is_featured(self): @override_settings(LANGUAGE_CODE="en-gb", LANGUAGES={"en-gb": "English"}) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationDetailPeriodOfOperationTestCase(TestCase): def setUp(self): self.station = mommy.make( @@ -197,6 +202,7 @@ def test_when_no_dates(self): @override_settings(SITE_ID=1) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationDetailSitesTestCase(TestCase): @classmethod def setUpTestData(cls): @@ -214,6 +220,7 @@ def test_hobbiton_detail_unavailable_on_site_2(self): self.assertEquals(response.status_code, 404) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class GentityFileDownloadLinkTestCase(TestCase): def setUp(self): self.station = mommy.make(Station, name="Komboti") @@ -227,6 +234,7 @@ def test_contains_download_link(self): self.assertContains(response, self.link) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class StationEditRedirectTestCase(TestCase): def setUp(self): self.response = self.client.get("/stations/42/edit/") @@ -240,6 +248,7 @@ def test_redirect_target(self): ) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class RedirectOldUrlsTestCase(TestCase): def test_old_stations_url_redirects(self): r = self.client.get("/stations/d/200348/") @@ -249,6 +258,7 @@ def test_old_stations_url_redirects(self): @skipUnless(getattr(settings, "SELENIUM_WEBDRIVERS", False), "Selenium is unconfigured") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class ListStationsVisibleOnMapTestCase(SeleniumTestCase): button_limit_to_map = PageElement(By.ID, "limit-to-map") @@ -300,6 +310,7 @@ def test_list_stations_visible_on_map(self): @skipUnless(getattr(settings, "SELENIUM_WEBDRIVERS", False), "Selenium is unconfigured") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class ShowOnlySearchedForStationsOnMapTestCase(SeleniumTestCase): markers = PageElement(By.CSS_SELECTOR, ".leaflet-marker-pane") @@ -340,6 +351,7 @@ def _get_num_stations_shown(self): @skipUnless(getattr(settings, "SELENIUM_WEBDRIVERS", False), "Selenium is unconfigured") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class ShowStationOnStationDetailMapTestCase(SeleniumTestCase): markers = PageElement(By.CSS_SELECTOR, ".leaflet-marker-pane") diff --git a/enhydris/tests/test_views/test_timeseries.py b/enhydris/tests/test_views/test_timeseries.py index 0940f4a4..d77d85c2 100644 --- a/enhydris/tests/test_views/test_timeseries.py +++ b/enhydris/tests/test_views/test_timeseries.py @@ -1,8 +1,9 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from enhydris.tests import TimeseriesDataMixin +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesDownloadButtonTestCase(TimeseriesDataMixin, TestCase): def setUp(self): self.download_button = ( @@ -35,6 +36,7 @@ def test_shows_unavailability_message_when_timeseries_data_is_unavailable(self): self.assertContains(self.response, "No data is available for downloading") +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class DownloadDataTestCase(TimeseriesDataMixin, TestCase): def setUp(self): self.create_timeseries(publicly_available=True) diff --git a/enhydris/tests/test_views/test_timeseries_group.py b/enhydris/tests/test_views/test_timeseries_group.py index 0aa7ce83..6bc65e64 100644 --- a/enhydris/tests/test_views/test_timeseries_group.py +++ b/enhydris/tests/test_views/test_timeseries_group.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from model_mommy import mommy @@ -6,6 +6,7 @@ from enhydris.tests import TimeseriesDataMixin +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class RedirectOldUrlsTestCase(TestCase): def test_old_timeseries_url_redirects(self): mommy.make( @@ -27,6 +28,7 @@ def test_old_timeseries_url_for_nonexistent_timeseries_returns_404(self): self.assertEqual(r.status_code, 404) +@override_settings(ENHYDRIS_AUTHENTICATION_REQUIRED=False) class TimeseriesGroupDetailTestCase(TimeseriesDataMixin, TestCase): @classmethod def setUpTestData(cls): diff --git a/enhydris_project/settings/__init__.py b/enhydris_project/settings/__init__.py index 38c898dc..ad4c5d5d 100644 --- a/enhydris_project/settings/__init__.py +++ b/enhydris_project/settings/__init__.py @@ -55,6 +55,7 @@ "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.middleware.gzip.GZipMiddleware", + "enhydris.middleware.GlobalLoginRequiredMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", "crequest.middleware.CrequestMiddleware", @@ -122,6 +123,7 @@ # https://github.com/mbi/django-simple-captcha/issues/84 CAPTCHA_TEST_MODE = len(sys.argv) > 1 and sys.argv[1] == "test" +ENHYDRIS_AUTHENTICATION_REQUIRED = False ENHYDRIS_USERS_CAN_ADD_CONTENT = False ENHYDRIS_DEFAULT_PUBLICLY_AVAILABLE = True ENHYDRIS_ENABLE_TIMESERIES_DATA_VIEWERS = False