From 64a216cbf5cce5ecb363d3b0188852798517660a Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 12:34:39 +0100 Subject: [PATCH 01/10] add pz code to submission table for superusers --- project/npda/templates/partials/submission_history.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/npda/templates/partials/submission_history.html b/project/npda/templates/partials/submission_history.html index 5fd51c09..48ece97c 100644 --- a/project/npda/templates/partials/submission_history.html +++ b/project/npda/templates/partials/submission_history.html @@ -16,6 +16,7 @@

All Submi Audit Year Patient Number Active + {% if request.user.view_preference == 2 %}PDU{% endif %} Download @@ -41,6 +42,7 @@

All Submi {% endif %} + {% if request.user.view_preference == 2 %}{{ submission.paediatric_diabetes_unit.pz_code }} ({{submission.paediatric_diabetes_unit.lead_organisation_name}}){% endif %}
{% csrf_token %} @@ -61,7 +63,7 @@

All Submi type="submit" class="join-item bg-rcpch_red text-white font-semibold hover:text-white py-1 px-2 border border-rcpch_red hover:bg-rcpch_red_dark_tint hover:border-rcpch_red_dark_tint btn-sm rounded-none {% if submission.submission_active %}opacity-50 cursor-not-allowed {% endif %}">Delete - {% if forloop.first %} + {% if submission.submission_active %} Date: Sun, 15 Sep 2024 12:43:54 +0100 Subject: [PATCH 02/10] hide delete button from active submissions --- .../partials/submission_history.html | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/project/npda/templates/partials/submission_history.html b/project/npda/templates/partials/submission_history.html index 48ece97c..590718cf 100644 --- a/project/npda/templates/partials/submission_history.html +++ b/project/npda/templates/partials/submission_history.html @@ -48,20 +48,19 @@

All Submi {% csrf_token %} {% if submission.submission_active %} - + + {% else %} + {% endif %} - {% if submission.submission_active %} From 1e2edd1b7fc5a10385b3bd18be9e187ad04a781b Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 12:52:35 +0100 Subject: [PATCH 03/10] add new activities to VisitActivity and more readable message in table --- project/npda/models/visitactivity.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/project/npda/models/visitactivity.py b/project/npda/models/visitactivity.py index e2c52b94..77a9eb11 100644 --- a/project/npda/models/visitactivity.py +++ b/project/npda/models/visitactivity.py @@ -7,11 +7,21 @@ class VisitActivity(models.Model): SUCCESSFUL_LOGIN = 1 UNSUCCESSFUL_LOGIN = 2 LOGOUT = 3 + PASSWORD_RESET_LINK_SENT = 4 + PASSWORD_RESET = 5 + SETUP_TWO_FACTOR_AUTHENTICATION = 5 + UPLOADED_CSV = 6 + TOUCHED_PATIENT_RECORD = 7 ACTIVITY = ( - (SUCCESSFUL_LOGIN, "SUCCESSFUL_LOGIN"), - (UNSUCCESSFUL_LOGIN, "UNSUCCESSFUL_LOGIN"), - (LOGOUT, "LOGOUT"), + (SUCCESSFUL_LOGIN, "Successful login"), + (UNSUCCESSFUL_LOGIN, "Login failed"), + (LOGOUT, "Logout"), + (PASSWORD_RESET_LINK_SENT, "Password Reset link sent"), + (PASSWORD_RESET, "Password reset"), + (SETUP_TWO_FACTOR_AUTHENTICATION, "Two factor authentication set up"), + (UPLOADED_CSV, "Uploaded CSV"), + (TOUCHED_PATIENT_RECORD, "Touched patient record"), ) activity_datetime = models.DateTimeField(auto_created=True, default=timezone.now) From 53c49562ea3a4d7900adff1e4937de1bb6e5ac56 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 13:44:16 +0100 Subject: [PATCH 04/10] update visitactivity to record password reset link and successful password reset bug fix - password_last_reset previously resent when email sent, now updated when actually reset --- project/npda/forms/npda_user_form.py | 12 ++++++------ project/npda/models/visitactivity.py | 10 +++++----- project/npda/signals.py | 19 +++++++++++++++++++ project/npda/templates/npda_user_logs.html | 3 +++ project/npda/views/npda_users.py | 18 ++++++++++++++++-- 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/project/npda/forms/npda_user_form.py b/project/npda/forms/npda_user_form.py index 3641231d..1dffb325 100644 --- a/project/npda/forms/npda_user_form.py +++ b/project/npda/forms/npda_user_form.py @@ -12,14 +12,9 @@ # third party imports from captcha.fields import CaptchaField -from project.npda.general_functions import organisations_adapter - # RCPCH imports from ...constants.styles.form_styles import * -from ..models import NPDAUser -from project.npda.general_functions import ( - organisations_adapter, -) +from ..models import NPDAUser, VisitActivity # Logging setup @@ -126,6 +121,11 @@ def save(self, *args, commit=True, **kwargs): user.password_last_set = timezone.now() if commit: logger.debug(f"Updating password_last_set to {timezone.now()}") + VisitActivity.objects.create( + npdauser=user, + activity=5, + ip_address=None, # cannot get ip address here as it is not a request + ) # password reset successful - activity 5 user.save() return user diff --git a/project/npda/models/visitactivity.py b/project/npda/models/visitactivity.py index 77a9eb11..335ebd05 100644 --- a/project/npda/models/visitactivity.py +++ b/project/npda/models/visitactivity.py @@ -9,16 +9,16 @@ class VisitActivity(models.Model): LOGOUT = 3 PASSWORD_RESET_LINK_SENT = 4 PASSWORD_RESET = 5 - SETUP_TWO_FACTOR_AUTHENTICATION = 5 - UPLOADED_CSV = 6 - TOUCHED_PATIENT_RECORD = 7 + SETUP_TWO_FACTOR_AUTHENTICATION = 6 + UPLOADED_CSV = 7 + TOUCHED_PATIENT_RECORD = 8 ACTIVITY = ( (SUCCESSFUL_LOGIN, "Successful login"), (UNSUCCESSFUL_LOGIN, "Login failed"), (LOGOUT, "Logout"), - (PASSWORD_RESET_LINK_SENT, "Password Reset link sent"), - (PASSWORD_RESET, "Password reset"), + (PASSWORD_RESET_LINK_SENT, "Password reset link sent"), + (PASSWORD_RESET, "Password reset successfully"), (SETUP_TWO_FACTOR_AUTHENTICATION, "Two factor authentication set up"), (UPLOADED_CSV, "Uploaded CSV"), (TOUCHED_PATIENT_RECORD, "Touched patient record"), diff --git a/project/npda/signals.py b/project/npda/signals.py index 86c439d7..7a1173f7 100644 --- a/project/npda/signals.py +++ b/project/npda/signals.py @@ -17,6 +17,15 @@ # Logging setup logger = logging.getLogger(__name__) +""" +This file contains signals that are triggered when a user logs in, logs out, or fails to log in. +These signals are used to log user activity in the VisitActivity model. +They are also used to track if users have touched patient records. +""" + +# Custom signals +from django.dispatch import Signal + @receiver(user_logged_in) def log_user_login(sender, request, user, **kwargs): @@ -66,6 +75,16 @@ def log_user_logout(sender, request, user, **kwargs): ) +# @receiver(password_reset_sent) +# def log_password_reset(sender, request, user, **kwargs): +# logger.info( +# f"Password reset link sent to {user} ({user.email}) from {get_client_ip(request)}." +# ) +# VisitActivity.objects.create( +# activity=4, ip_address=get_client_ip(request), npdauser=user +# ) + + # helper functions def get_client_ip(request): return request.META.get("REMOTE_ADDR") diff --git a/project/npda/templates/npda_user_logs.html b/project/npda/templates/npda_user_logs.html index de06bd0d..baa80e22 100644 --- a/project/npda/templates/npda_user_logs.html +++ b/project/npda/templates/npda_user_logs.html @@ -6,6 +6,9 @@
NPDA Audit Access Logs for {{npdauser.get_full_name}} (NPDA User ID-{{npdauser.pk}}) +

+ Password last set: {{npdauser.password_last_set}} +

{% if visitactivities %} diff --git a/project/npda/views/npda_users.py b/project/npda/views/npda_users.py index a68d85a2..5e816c75 100644 --- a/project/npda/views/npda_users.py +++ b/project/npda/views/npda_users.py @@ -40,6 +40,8 @@ from project.constants import VIEW_PREFERENCES from .mixins import LoginAndOTPRequiredMixin +# from ..signals import password_reset_sent + logger = logging.getLogger(__name__) """ @@ -450,6 +452,10 @@ def get_context_data(self, **kwargs): class ResetPasswordView(SuccessMessageMixin, PasswordResetView): + """ + Custom password reset view that sends a password reset email to the user + """ + template_name = "registration/password_reset.html" html_email_template_name = "registration/password_reset_email.html" email_template_name = strip_tags("registration/password_reset_email.html") @@ -468,8 +474,16 @@ class ResetPasswordView(SuccessMessageMixin, PasswordResetView): # extend form_valid to set user.password_last_set def form_valid(self, form): - self.request.user.password_last_set = timezone.now() - + # self.request.user.password_last_set = timezone.now() + user_email_to_reset_password = form.cleaned_data["email"] + # check if user exists + if NPDAUser.objects.filter(email=user_email_to_reset_password).exists(): + user = NPDAUser.objects.get(email=user_email_to_reset_password) + VisitActivity.objects.create( + npdauser=user, + activity=4, + ip_address=self.request.META.get("REMOTE_ADDR"), + ) # password reset link sent return super().form_valid(form) From 8f186994b7a441013ef87b31cd7d8881bda77a02 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 14:17:51 +0100 Subject: [PATCH 05/10] update documentation for users on passwords and login remove password reset for logged in users activate lockout for 5 minutes with 5 failed logins store this in visit activity --- documentation/docs/developer/users.md | 22 +++++++++++++- project/npda/forms/npda_user_form.py | 43 +++++++++++++++------------ project/npda/models/visitactivity.py | 8 +++-- project/npda/templates/nav.html | 3 +- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/documentation/docs/developer/users.md b/documentation/docs/developer/users.md index 19bf6932..65c92290 100644 --- a/documentation/docs/developer/users.md +++ b/documentation/docs/developer/users.md @@ -68,4 +68,24 @@ This has the basic django user functions but has the following extra custome fie - `is_rcpch_staff`: boolean - a custom field that defines the user is an RCPCH staff member. This is as opposed to a clinician who may be a member of the audit team, but not an RCPCH employee - `is_patient_or_carer`: boolean - a custom field that defines the user is a patient or carer - `role` - user type as above -- `organisation_employer` - this is a relational field with an Organisation. Only applies to clinicians and therefore is None for RCPCH employees. \ No newline at end of file +- `organisation_employer` - this is a relational field with an Organisation. Only applies to clinicians and therefore is None for RCPCH employees. + +#### Passwords and Two factor authentication + +Password access is required to access all areas of the NPDA platform apart from the documentation/user guide. Rules for passwords are: +Minimum of 10 characters (minimum 16 for RCPCH Audit team) +Must contain ONE capital +Must contain ONE number +Must contain ONE symbol from !@£$%^&*()_-+=|~ +Must NOT be exclusively numbers +Must NOT be same as your email, name, surname + +User accounts allow a maximum of 5 consecutive attempts after which the account is locked for 5 minutes. + +Two Factor authentication is required for all login access. This is set up only once at first login. A user can change their 2 Factor Authentication settings once logged in by clicking on the their name in the top right of the screen and navigating to Two Factor Authentication. + +Two Factor Authentication is either by email or Microsoft Authenticator on a mobile phone. If a user successfully logs in with their passwords, they must either check their email for a Token or generate one on their Microsoft Authenticator app. + +#### Captcha + +In addition to the above methods of authentication, a rotating image of numbers or letters is used to ensure only humans can gain access. diff --git a/project/npda/forms/npda_user_form.py b/project/npda/forms/npda_user_form.py index 1dffb325..8c5d973a 100644 --- a/project/npda/forms/npda_user_form.py +++ b/project/npda/forms/npda_user_form.py @@ -171,23 +171,28 @@ def clean_username(self) -> dict[str]: user = NPDAUser.objects.get(email=email.lower()) - # visit_activities = VisitActivity.objects.filter( - # npda12user=user - # ).order_by("-activity_datetime")[:5] - - # failed_login_activities = [ - # activity for activity in visit_activities if activity.activity == 2 - # ] - - # if failed_login_activities: - # first_activity = failed_login_activities[-1] - - # if len( - # failed_login_activities - # ) >= 5 and timezone.now() <= first_activity.activity_datetime + timezone.timedelta( - # minutes=10 - # ): - # raise forms.ValidationError( - # "You have failed to login 5 or more consecutive times. You have been locked out for 10 minutes" - # ) + visit_activities = VisitActivity.objects.filter(npdauser=user).order_by( + "-activity_datetime" + )[:5] + + failed_login_activities = [ + activity for activity in visit_activities if activity.activity == 2 + ] + + if failed_login_activities: + first_activity = failed_login_activities[-1] + + if len( + failed_login_activities + ) >= 5 and timezone.now() <= first_activity.activity_datetime + timezone.timedelta( + minutes=5 + ): + VisitActivity.objects.create( + activity=6, + ip_address=self.request.META.get("REMOTE_ADDR"), + npdauser=user, # password lockout - activity 6 + ) + raise forms.ValidationError( + "You have failed to login 5 or more consecutive times. You have been locked out for 10 minutes" + ) return email.lower() diff --git a/project/npda/models/visitactivity.py b/project/npda/models/visitactivity.py index 335ebd05..23012101 100644 --- a/project/npda/models/visitactivity.py +++ b/project/npda/models/visitactivity.py @@ -9,9 +9,10 @@ class VisitActivity(models.Model): LOGOUT = 3 PASSWORD_RESET_LINK_SENT = 4 PASSWORD_RESET = 5 - SETUP_TWO_FACTOR_AUTHENTICATION = 6 - UPLOADED_CSV = 7 - TOUCHED_PATIENT_RECORD = 8 + PASSWORD_LOCKOUT = 6 + SETUP_TWO_FACTOR_AUTHENTICATION = 7 + UPLOADED_CSV = 8 + TOUCHED_PATIENT_RECORD = 9 ACTIVITY = ( (SUCCESSFUL_LOGIN, "Successful login"), @@ -19,6 +20,7 @@ class VisitActivity(models.Model): (LOGOUT, "Logout"), (PASSWORD_RESET_LINK_SENT, "Password reset link sent"), (PASSWORD_RESET, "Password reset successfully"), + (PASSWORD_LOCKOUT, "Password lockout"), (SETUP_TWO_FACTOR_AUTHENTICATION, "Two factor authentication set up"), (UPLOADED_CSV, "Uploaded CSV"), (TOUCHED_PATIENT_RECORD, "Touched patient record"), diff --git a/project/npda/templates/nav.html b/project/npda/templates/nav.html index b7acf89e..847d379c 100644 --- a/project/npda/templates/nav.html +++ b/project/npda/templates/nav.html @@ -53,8 +53,7 @@
From 3e9ad6582b536dd5bcc7058a1f03db54d9f1ed68 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 14:44:36 +0100 Subject: [PATCH 08/10] store csv upload in VisitActivity --- project/npda/views/home.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/project/npda/views/home.py b/project/npda/views/home.py index d8ed1e7c..5d2eb578 100644 --- a/project/npda/views/home.py +++ b/project/npda/views/home.py @@ -2,10 +2,11 @@ import logging # Django imports -from django.urls import reverse +from django.apps import apps from django.contrib import messages -from django.shortcuts import render from django.core.exceptions import ValidationError +from django.shortcuts import render +from django.urls import reverse # HTMX imports from django_htmx.http import trigger_client_event @@ -63,6 +64,12 @@ def home(request): pdu_pz_code=pz_code, ) messages.success(request=request, message="File uploaded successfully") + VisitActivity = apps.get_model("npda", "VisitActivity") + VisitActivity.objects.create( + activity=8, + ip_address=request.META.get("REMOTE_ADDR"), + npdauser=request.user, + ) # uploaded csv - activity 8 except ValidationError as error: errors = error_list(error) From 1cf8a6d9d3c2ff7f6fc0091d29a1f5bf55821871 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 14:47:58 +0100 Subject: [PATCH 09/10] signpost update patient list to toggle between national/PDU in template --- project/npda/templates/partials/patient_table.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/project/npda/templates/partials/patient_table.html b/project/npda/templates/partials/patient_table.html index 41040d90..dbf93e04 100644 --- a/project/npda/templates/partials/patient_table.html +++ b/project/npda/templates/partials/patient_table.html @@ -1,7 +1,14 @@ {% load static %} {% load npda_tags %} {% url 'patients' as patients_url %} -

Patients under the care of {{pz_code}}

+{% if request.user.view_preference == 1 %} + +

Patients under the care of {{pz_code}}

+{% elif request.user.view_preference == 2 %} + +

All patients nationally

+{% endif %} + {% if patient_list %}
From 95583670f0a8ac4b3645411c29f724154f9f7f32 Mon Sep 17 00:00:00 2001 From: eatyourpeas Date: Sun, 15 Sep 2024 14:55:25 +0100 Subject: [PATCH 10/10] trap failed to save csv_upload at activity in error logs --- project/npda/views/home.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/project/npda/views/home.py b/project/npda/views/home.py index 5d2eb578..965c0bd0 100644 --- a/project/npda/views/home.py +++ b/project/npda/views/home.py @@ -65,11 +65,14 @@ def home(request): ) messages.success(request=request, message="File uploaded successfully") VisitActivity = apps.get_model("npda", "VisitActivity") - VisitActivity.objects.create( - activity=8, - ip_address=request.META.get("REMOTE_ADDR"), - npdauser=request.user, - ) # uploaded csv - activity 8 + try: + VisitActivity.objects.create( + activity=8, + ip_address=request.META.get("REMOTE_ADDR"), + npdauser=request.user, + ) # uploaded csv - activity 8 + except Exception as e: + logger.error(f"Failed to log user activity: {e}") except ValidationError as error: errors = error_list(error)