diff options
-rw-r--r-- | CHANGES.md | 1 | ||||
-rw-r--r-- | example_project/settings.py | 3 | ||||
-rw-r--r-- | ishtar_common/context_processors.py | 36 | ||||
-rw-r--r-- | ishtar_common/migrations/0227_auto_20230404_1112.py | 29 | ||||
-rw-r--r-- | ishtar_common/models.py | 5 | ||||
-rw-r--r-- | ishtar_common/templates/base.html | 2 | ||||
-rw-r--r-- | ishtar_common/urls_registration.py | 6 | ||||
-rw-r--r-- | ishtar_common/views.py | 42 | ||||
-rw-r--r-- | ishtar_common/wizards.py | 1 |
9 files changed, 113 insertions, 12 deletions
diff --git a/CHANGES.md b/CHANGES.md index 2ccc2d3ba..8a21b2e98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Ishtar changelog ### Features/improvements ### - Load task refactoring - manage external_id regen with tasks - Containers: manage history +- Manage expiration of passwords ### Bug fixes ### - Json fields: fix bad save of multi values diff --git a/example_project/settings.py b/example_project/settings.py index 0aafbdf90..934582b9b 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -276,6 +276,7 @@ JOINT = " | " # not managed cautiously the dir contening these scripts is not set by default ISHTAR_SCRIPT_DIR = "" +# TODO: clean... ISHTAR_FILE_PREFIX = "" ISHTAR_OPE_PREFIX = "OA" ISHTAR_DEF_OPE_PREFIX = "OP" @@ -289,6 +290,8 @@ ISHTAR_PERIODS = {} ISHTAR_PERMIT_TYPES = {} ISHTAR_DOC_TYPES = {"undefined": "Undefined"} +# number of days for password expiration: None for no expiration +ISHTAR_PASSWORD_EXPIRATION_DAYS = None ISHTAR_SEARCH_LANGUAGE = "french" ISHTAR_SECURE = True ISHTAR_SECURE_OPTIONS = False diff --git a/ishtar_common/context_processors.py b/ishtar_common/context_processors.py index e1754e935..c1d1224ea 100644 --- a/ishtar_common/context_processors.py +++ b/ishtar_common/context_processors.py @@ -17,8 +17,13 @@ # See the file COPYING for details. +import datetime + from django.conf import settings +from django.core.cache import cache from django.contrib.sites.models import Site +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ from ishtar_common.version import __version__ from ishtar_common.models import get_current_profile @@ -41,13 +46,6 @@ def get_base_context(request): except Site.DoesNotExist: dct["APP_NAME"] = settings.APP_NAME dct["COUNTRY"] = settings.COUNTRY - """ - if 'MENU' not in request.session or \ - request.session['MENU'].user != request.user: - menu = Menu(request.user) - menu.init() - request.session['MENU'] = menu - """ # menu is now in cache - put it back in session later? current_action = None if "CURRENT_ACTION" in request.session: @@ -71,6 +69,30 @@ def get_base_context(request): menu.init() if hasattr(request.user, "ishtaruser") and request.user.ishtaruser: + + # check password expiration date + if settings.ISHTAR_PASSWORD_EXPIRATION_DAYS and \ + isinstance(settings.ISHTAR_PASSWORD_EXPIRATION_DAYS, int): + key = f"{settings.PROJECT_SLUG}-password_expired-{request.user.pk}" + password_expired = cache.get(key) + if password_expired is None: + password_expired = True + d = datetime.date.today() - request.user.ishtaruser.password_last_update + if d.days < settings.ISHTAR_PASSWORD_EXPIRATION_DAYS: + password_expired = False + cache.set(key, password_expired, settings.CACHE_TIMEOUT) + if password_expired and not request.path.endswith("password_change/"): + msg = str(_("Your password has expired. Please update it using this " + "form.")) + msg = msg.replace( + str(_("form")), + f'<a href="{reverse("password_change")}">' + f'<i class="fa fa-external-link" aria-hidden="true"></i> ' + f'{_("form")}</a>' + ) + dct["MESSAGES"].append((msg, "warning")) + + # external sources if ( request.user.ishtaruser.current_profile and "EXTERNAL_SOURCES" not in request.session diff --git a/ishtar_common/migrations/0227_auto_20230404_1112.py b/ishtar_common/migrations/0227_auto_20230404_1112.py new file mode 100644 index 000000000..1ba74f292 --- /dev/null +++ b/ishtar_common/migrations/0227_auto_20230404_1112.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.24 on 2023-04-04 11:12 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0226_auto_20230316_1115'), + ] + + operations = [ + migrations.AddField( + model_name='ishtaruser', + name='password_last_update', + field=models.DateField(default=datetime.date.today, verbose_name='Password last update'), + ), + migrations.AlterField( + model_name='customform', + name='header', + field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Header text'), + ), + migrations.AlterField( + model_name='ishtarsiteprofile', + name='footer', + field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Footer text'), + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index c053f7c10..efa061431 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -3410,6 +3410,9 @@ class IshtarUser(FullSearch): related_name="ishtaruser", on_delete=models.CASCADE, ) + password_last_update = models.DateField( + _("Password last update"), default=datetime.date.today + ) advanced_shortcut_menu = models.BooleanField( _("Advanced shortcut menu"), default=False ) @@ -3491,6 +3494,8 @@ class IshtarUser(FullSearch): def import_set_password(self, context, value): self.user_ptr.set_password(value) self.user_ptr.save() + self.password_last_update = datetime.date.today() + self.save() @post_importer_action def import_create_profile(self, context, value): diff --git a/ishtar_common/templates/base.html b/ishtar_common/templates/base.html index 53ef4f35a..be7187ef1 100644 --- a/ishtar_common/templates/base.html +++ b/ishtar_common/templates/base.html @@ -209,7 +209,7 @@ {% if MESSAGES %}{% for message, message_type in MESSAGES %} <div class="alert alert-{{message_type}} alert-dismissible fade show" role="alert"> - {{message}} + {{message|safe}} <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> diff --git a/ishtar_common/urls_registration.py b/ishtar_common/urls_registration.py index 0a18ff060..29d0055d7 100644 --- a/ishtar_common/urls_registration.py +++ b/ishtar_common/urls_registration.py @@ -39,9 +39,9 @@ urlpatterns = [ name='registration_disallowed'), # url("^accounts/", include('django.contrib.auth.urls')), path('accounts/login/', views.LoginView.as_view(), name='login'), - path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), + path('accounts/logout/', views.LogoutView.as_view(), name='logout'), - path('accounts/password_change/', auth_views.PasswordChangeView.as_view(), + path('accounts/password_change/', views.PasswordChangeView.as_view(), name='password_change'), path('accounts/password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), @@ -49,7 +49,7 @@ urlpatterns = [ path('accounts/password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), path('accounts/password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), - path('accounts/reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), + path('accounts/reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('accounts/reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), diff --git a/ishtar_common/views.py b/ishtar_common/views.py index b6b58fe05..f5a58afad 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -31,9 +31,13 @@ from django.apps import apps from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required -from django.contrib.auth.views import redirect_to_login, LoginView as AuthLoginView +from django.contrib.auth.views import redirect_to_login, LoginView as AuthLoginView, \ + PasswordChangeView as AuthPasswordChangeView, \ + PasswordResetConfirmView as AuthPasswordResetConfirmView, \ + LogoutView as AuthLogoutView from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist +from django.core.cache import cache from django.db.models import Q from django.template import loader from django.forms.models import modelformset_factory @@ -171,6 +175,42 @@ class LoginView(AuthLoginView): form_class = forms.AuthenticationForm +class LogoutView(AuthLogoutView): + def get(self, request, *args, **kwargs): + # clear cache + keys = [] + if request.user and hasattr(request.user, "pk") and request.user.pk: + keys.append(f"{settings.PROJECT_SLUG}-password_expired-{request.user.pk}") + for key in keys: + cache.delete(key) + return super().get(request, *args, **kwargs) + + +def update_password_last_update(user): + try: + ishtar_user = models.IshtarUser.objects.get(pk=user.pk) + except models.IshtarUser.DoesNotExist: + return + ishtar_user.password_last_update = datetime.date.today() + ishtar_user.save() + key = f"{settings.PROJECT_SLUG}-password_expired-{ishtar_user.pk}" + cache.set(key, False, settings.CACHE_TIMEOUT) + + +class PasswordChangeView(AuthPasswordChangeView): + def form_valid(self, form): + returned = super().form_valid(form) + update_password_last_update(form.user) + return returned + + +class PasswordResetConfirmView(AuthPasswordResetConfirmView): + def form_valid(self, form): + returned = super().form_valid(form) + update_password_last_update(form.user) + return returned + + person_search_wizard = wizards.PersonSearch.as_view( [("general-person_search", forms.PersonFormSelection)], label=_("Person search"), diff --git a/ishtar_common/wizards.py b/ishtar_common/wizards.py index f6b2894ab..72cd4db45 100644 --- a/ishtar_common/wizards.py +++ b/ishtar_common/wizards.py @@ -2040,6 +2040,7 @@ class AccountWizard(Wizard): if dct["password"]: account.set_password(dct["password"]) + account.password_last_update = datetime.date.today() account.save() profile_form = form_dict["profile-account_management"] |