diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2342f7d..7ef07d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,7 @@ jobs: - '3.9' - '3.10' - '3.11' + - '3.12' steps: - uses: actions/checkout@v4 diff --git a/docs/topics/setup.rst b/docs/topics/setup.rst index 35e8b1a..7742eaf 100644 --- a/docs/topics/setup.rst +++ b/docs/topics/setup.rst @@ -195,10 +195,10 @@ file:: Serializer ============================ -For now this app uses the PickleSerializer. This needs to be set up in the Django settings -file:: +This app is tested with both PickleSerializer and JsonSerializer. - SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer' +Django recommends to change from old pickle serializer to json because +possible remote code execution vulnerability. .. _setup-create-db-tables: diff --git a/password_policies/conf/settings.py b/password_policies/conf/settings.py index ae7d78e..1a37f29 100644 --- a/password_policies/conf/settings.py +++ b/password_policies/conf/settings.py @@ -184,3 +184,8 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1 + +PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY = "_password_policies_last_checked" +PASSWORD_POLICIES_EXPIRED_SESSION_KEY = "_password_policies_expired" +PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY = "_password_policies_last_changed" +PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY = "_password_policies_change_required" diff --git a/password_policies/context_processors.py b/password_policies/context_processors.py index 9a5f41b..50d37ef 100644 --- a/password_policies/context_processors.py +++ b/password_policies/context_processors.py @@ -1,3 +1,4 @@ +from password_policies.conf import settings from password_policies.models import PasswordHistory @@ -29,9 +30,9 @@ def password_status(request): auth = auth() if auth: - if '_password_policies_change_required' not in request.session: + if settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY not in request.session: r = PasswordHistory.objects.change_required(request.user) else: - r = request.session['_password_policies_change_required'] + r = request.session[settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY] d['password_change_required'] = r return d diff --git a/password_policies/middleware.py b/password_policies/middleware.py index 2a1bb03..3efd6a9 100644 --- a/password_policies/middleware.py +++ b/password_policies/middleware.py @@ -20,8 +20,7 @@ from password_policies.conf import settings from password_policies.models import PasswordChangeRequired, PasswordHistory -from password_policies.utils import PasswordCheck - +from password_policies.utils import PasswordCheck, string_to_datetime, datetime_to_string class PasswordChangeMiddleware(MiddlewareMixin): """ @@ -70,22 +69,24 @@ class PasswordChangeMiddleware(MiddlewareMixin): This middleware does not try to redirect using the HTTPS protocol.""" - checked = "_password_policies_last_checked" - expired = "_password_policies_expired" - last = "_password_policies_last_changed" - required = "_password_policies_change_required" + checked = settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY + expired = settings.PASSWORD_POLICIES_EXPIRED_SESSION_KEY + last = settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY + required = settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY td = timedelta(seconds=settings.PASSWORD_DURATION_SECONDS) def _check_history(self, request): if not request.session.get(self.last, None): newest = PasswordHistory.objects.get_newest(request.user) if newest: - request.session[self.last] = newest.created + request.session[self.last] = datetime_to_string(newest.created) else: # TODO: This relies on request.user.date_joined which might not # be available!!! - request.session[self.last] = request.user.date_joined - if request.session[self.last] < self.expiry_datetime: + request.session[self.last] = datetime_to_string(request.user.date_joined) + + date_last = string_to_datetime(request.session[self.last]) + if date_last < self.expiry_datetime: request.session[self.required] = True if not PasswordChangeRequired.objects.filter(user=request.user).count(): PasswordChangeRequired.objects.create(user=request.user) @@ -95,20 +96,22 @@ def _check_history(self, request): def _check_necessary(self, request): if not request.session.get(self.checked, None): - request.session[self.checked] = self.now + request.session[self.checked] = datetime_to_string(self.now) # If the PASSWORD_CHECK_ONLY_AT_LOGIN is set, then only check at the beginning of session, which we can # tell by self.now time having just been set. if ( not settings.PASSWORD_CHECK_ONLY_AT_LOGIN - or request.session.get(self.checked, None) == self.now + or request.session.get(self.checked, None) == datetime_to_string(self.now) ): # If a password change is enforced we won't check # the user's password history, thus reducing DB hits... if PasswordChangeRequired.objects.filter(user=request.user).count(): request.session[self.required] = True return - if request.session[self.checked] < self.expiry_datetime: + + date_checked = string_to_datetime(request.session[self.checked]) + if date_checked < self.expiry_datetime: try: del request.session[self.last] del request.session[self.checked] @@ -116,6 +119,7 @@ def _check_necessary(self, request): del request.session[self.expired] except KeyError: pass + if settings.PASSWORD_USE_HISTORY: self._check_history(request) else: diff --git a/password_policies/tests/test_middleware.py b/password_policies/tests/test_middleware.py index a0adb55..0cc9bc5 100644 --- a/password_policies/tests/test_middleware.py +++ b/password_policies/tests/test_middleware.py @@ -10,6 +10,7 @@ except ImportError: from django.urls.base import reverse +from django.test.utils import override_settings from django.utils import timezone from password_policies.conf import settings @@ -71,3 +72,48 @@ def test_password_change_required_enforced_redirect(self): self.assertEqual(get_response_location(response["Location"]), self.redirect_url) self.client.logout() p.delete() + + +class PasswordPoliciesMiddlewareJsonSerializerTest(TestCase): + def setUp(self): + self.user = create_user() + self.redirect_url = "http://testserver/password/change/?next=/" + + def test_password_middleware_without_history(self): + seconds = settings.PASSWORD_DURATION_SECONDS - 60 + self.user.date_joined = get_datetime_from_delta(timezone.now(), seconds) + self.user.last_login = get_datetime_from_delta(timezone.now(), seconds) + self.user.save() + self.client.login(username="alice", password=passwords[-1]) + response = self.client.get(reverse("home")) + self.assertEqual(response.status_code, 200) + self.client.logout() + + def test_password_middleware_with_history(self): + create_password_history(self.user) + self.client.login(username="alice", password=passwords[-1]) + response = self.client.get(reverse("home"), follow=False) + self.assertEqual(response.status_code, 302) + self.assertEqual(get_response_location(response["Location"]), self.redirect_url) + self.client.logout() + PasswordHistory.objects.filter(user=self.user).delete() + + def test_password_middleware_enforced_redirect(self): + self.client.login(username="alice", password=passwords[-1]) + response = self.client.get(reverse("home"), follow=False) + self.assertEqual(response.status_code, 302) + self.assertEqual(get_response_location(response["Location"]), self.redirect_url) + self.client.logout() + + def test_password_change_required_enforced_redirect(self): + seconds = settings.PASSWORD_DURATION_SECONDS - 60 + self.user.date_joined = get_datetime_from_delta(timezone.now(), seconds) + self.user.last_login = get_datetime_from_delta(timezone.now(), seconds) + self.user.save() + p = PasswordChangeRequired.objects.create(user=self.user) + self.client.login(username="alice", password=passwords[-1]) + response = self.client.get(reverse("home"), follow=False) + self.assertEqual(response.status_code, 302) + self.assertEqual(get_response_location(response["Location"]), self.redirect_url) + self.client.logout() + p.delete() diff --git a/password_policies/tests/test_models.py b/password_policies/tests/test_models.py index b16f337..f032495 100644 --- a/password_policies/tests/test_models.py +++ b/password_policies/tests/test_models.py @@ -23,4 +23,4 @@ def test_password_history_expiration(self): self.assertEqual(count, settings.PASSWORD_HISTORY_COUNT) def test_password_history_recent_passwords(self): - self.failIf(PasswordHistory.objects.check_password(self.user, passwords[-1])) + self.assertFalse(PasswordHistory.objects.check_password(self.user, passwords[-1])) diff --git a/password_policies/tests/test_settings.py b/password_policies/tests/test_settings.py index 76cd93f..3fee098 100644 --- a/password_policies/tests/test_settings.py +++ b/password_policies/tests/test_settings.py @@ -1,9 +1,4 @@ import os -from distutils.version import LooseVersion - -from django import get_version - -django_version = get_version() DEBUG = False @@ -42,36 +37,24 @@ SITE_ID = 1 -# This is to maintain compatibility with Django 1.7 -if LooseVersion(django_version) < LooseVersion("1.8.0"): - TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), "templates"),) - TEMPLATE_CONTEXT_PROCESSORS = ( - "django.contrib.auth.context_processors.auth", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "django.contrib.messages.context_processors.messages", - "password_policies.context_processors.password_status", - ) - -else: - TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [ - os.path.join(os.path.dirname(__file__), "templates"), +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(os.path.dirname(__file__), "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.contrib.messages.context_processors.messages", + "password_policies.context_processors.password_status", ], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.contrib.auth.context_processors.auth", - "django.template.context_processors.debug", - "django.template.context_processors.i18n", - "django.contrib.messages.context_processors.messages", - "password_policies.context_processors.password_status", - ], - }, }, - ] + }, +] MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", @@ -82,7 +65,8 @@ "django.contrib.messages.middleware.MessageMiddleware", ) -SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" +SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" + MEDIA_URL = "/media/somewhere/" diff --git a/password_policies/tests/test_views.py b/password_policies/tests/test_views.py index 9aee9cc..bb5de5b 100644 --- a/password_policies/tests/test_views.py +++ b/password_policies/tests/test_views.py @@ -1,13 +1,20 @@ +from unittest import skipIf + +from django import VERSION as DJANGO_VERSION from django.core import signing from django.test import Client, TestCase, override_settings +from django.utils import timezone from django.urls.base import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from password_policies.conf import settings from password_policies.forms import PasswordPoliciesChangeForm from password_policies.models import PasswordHistory from password_policies.tests.lib import create_user, passwords +from password_policies.utils import string_to_datetime, datetime_to_string +from freezegun import freeze_time class PasswordChangeViewsTestCase(TestCase): def setUp(self): @@ -23,7 +30,7 @@ def test_password_change(self): self.client.login(username="alice", password=passwords[-1]) response = self.client.get(reverse("password_change")) self.assertEqual(response.status_code, 200) - self.failUnless( + self.assertTrue( isinstance(response.context["form"], PasswordPoliciesChangeForm) ) self.assertTemplateUsed(response, "registration/password_change_form.html") @@ -43,8 +50,11 @@ def test_password_change_failure(self): self.client.login(username="alice", password=passwords[-1]) response = self.client.post(reverse("password_change"), data=data) self.assertEqual(response.status_code, 200) - self.failIf(response.context["form"].is_valid()) - self.assertFormError(response, "form", field="old_password", errors=msg) + self.assertFalse(response.context["form"].is_valid()) + if DJANGO_VERSION > (4, 1): + self.assertFormError(response.context["form"], field="old_password", errors=msg) + else: + self.assertFormError(response, "form", field="old_password", errors=msg) self.client.logout() def test_password_change_success(self): @@ -100,8 +110,11 @@ def test_password_change_wrong_validators(self): self.client.login(username="alice", password=data["old_password"]) response = self.client.post(reverse("password_change"), data=data) self.assertEqual(response.status_code, 200) - self.failIf(response.context["form"].is_valid()) - self.assertFormError(response, "form", field="new_password2", errors=msg) + self.assertFalse(response.context["form"].is_valid()) + if DJANGO_VERSION > (4, 1): + self.assertFormError(response.context["form"], field="new_password2", errors=msg) + else: + self.assertFormError(response, "form", field="new_password2", errors=msg) self.client.logout() def test_password_reset_complete(self): @@ -112,6 +125,184 @@ def test_password_reset_complete(self): ) assert res.status_code == 200 + @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=False) + @freeze_time("2021-07-21T17:00:00.000000") + def test_pickle_serializer_set_datetime_USE_TZ_false(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + + @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=True) + @freeze_time("2021-07-21T17:00:00.000000") + def test_pickle_serializer_set_datetime_USE_TZ_true(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + + @skipIf(DJANGO_VERSION >= (5, 0), 'PickleSerializer not supported in this version') + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer', USE_TZ=True) + @freeze_time("2021-07-21T18:00:00.000000+0100") + def test_pickle_serializer_set_datetime_USE_TZ_true_localized(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=False) + @freeze_time("2021-07-21T17:00:00.000000") + def test_json_serializer_set_datetime_USE_TZ_false(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=True) + @freeze_time("2021-07-21T17:00:00.000000") + def test_json_serializer_set_datetime_USE_TZ_true(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + + + @override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.JSONSerializer', USE_TZ=True) + @freeze_time("2021-07-21T18:00:00.000000+0100") + def test_json_serializer_set_datetime_USE_TZ_true_localized(self): + data = { + "old_password": passwords[-1], + "new_password1": "Chah+pher9k", + "new_password2": "Chah+pher9k", + } + self.client.login(username="alice", password=data["old_password"]) + response = self.client.post(reverse("password_change"), data=data) + session = self.client.session + + # Assert session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY]), + timezone.now()) + # Assert session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY] + self.assertIsInstance(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], str) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + datetime_to_string(timezone.now())) + self.assertEqual(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY], + "2021-07-21T17:00:00.000000+0000") + self.assertEqual(string_to_datetime(session[settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY]), + timezone.now()) + class TestLOMixinView(TestCase): def test_lomixinview(self): diff --git a/password_policies/utils.py b/password_policies/utils.py index e404c8e..7023556 100644 --- a/password_policies/utils.py +++ b/password_policies/utils.py @@ -1,6 +1,7 @@ -from datetime import timedelta +from datetime import timedelta, datetime from django.utils import timezone +from django.conf import settings as django_settings from django.core.exceptions import ObjectDoesNotExist from password_policies.conf import settings @@ -46,3 +47,29 @@ def get_expiry_datetime(self): "Returns the date and time when the user's password has expired." seconds = settings.PASSWORD_DURATION_SECONDS return timezone.now() - timedelta(seconds=seconds) + +def datetime_to_string(value, format=None): + """ Transform datetime object in a string with input format +:returns: formatted datetime +:rtype: str +""" + if format is None: + format = "%Y-%m-%dT%H:%M:%S.%f%z" if django_settings.USE_TZ else "%Y-%m-%dT%H:%M:%S.%f" + + if not isinstance(value, str): + return datetime.strftime(value, format) + else: + return value + +def string_to_datetime(value, format=None): + """ Transform string object in a datetime with input format +:returns: formatted string +:rtype: datetime +""" + if format is None: + format = "%Y-%m-%dT%H:%M:%S.%f%z" if django_settings.USE_TZ else "%Y-%m-%dT%H:%M:%S.%f" + + if not isinstance(value, datetime): + return datetime.strptime(value, format) + else: + return value diff --git a/password_policies/views.py b/password_policies/views.py index a630c27..7b4ad68 100644 --- a/password_policies/views.py +++ b/password_policies/views.py @@ -34,7 +34,7 @@ PasswordPoliciesForm, PasswordResetForm, ) - +from password_policies.utils import string_to_datetime, datetime_to_string class LoggedOutMixin(View): """ @@ -110,12 +110,14 @@ def get_success_url(self): user was requesting before the password change.) If not returns the :attr:`~PasswordChangeFormView.success_url` attribute if set, otherwise the URL to the :class:`PasswordChangeDoneView`.""" - checked = "_password_policies_last_checked" - last = "_password_policies_last_changed" - required = "_password_policies_change_required" + checked = settings.PASSWORD_POLICIES_LAST_CHECKED_SESSION_KEY + last = settings.PASSWORD_POLICIES_LAST_CHANGED_SESSION_KEY + required = settings.PASSWORD_POLICIES_CHANGE_REQUIRED_SESSION_KEY now = timezone.now() - self.request.session[checked] = now - self.request.session[last] = now + now_str = datetime_to_string(now) + + self.request.session[checked] = now_str + self.request.session[last] = now_str self.request.session[required] = False redirect_to = self.request.POST.get(self.redirect_field_name, "") if redirect_to: diff --git a/tox.ini b/tox.ini index 1a271b8..2fcc06e 100644 --- a/tox.ini +++ b/tox.ini @@ -15,14 +15,23 @@ envlist = py310-django40 py39-django41 py310-django41 + py310-django42 + py311-django42 + py312-django42 + py310-django50 + py311-django50 + py312-django50 [testenv] deps = + django50: Django>=5.0,<5.1 + django42: Django>=4.2,<4.3 django41: Django>=4.1,<4.2 django40: Django>=4.0,<4.1 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 django22: Django>=2.2,<2.3 + freezegun pytest pytest-django pytest-cov