From 56a3ca4c99f2013c973396f83f3a908144c472d1 Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 7 Nov 2026 14:26:06 +0300 Subject: [PATCH] fix(logs): a lost-OTP mail and a wrong old password leave a row The "Lost OTP?" page writes a row when it mails a user their QR code link, or fails to, under the anonymous requester and their address. A password change refused for a wrong old password writes a failed row. --- accounts/tests/test_audit_rows.py | 10 ++++++++++ accounts/tests/test_email_otp.py | 6 ++++++ accounts/views.py | 6 +++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/accounts/tests/test_audit_rows.py b/accounts/tests/test_audit_rows.py index c75a4f42..57764be5 100644 --- a/accounts/tests/test_audit_rows.py +++ b/accounts/tests/test_audit_rows.py @@ -25,6 +25,16 @@ class OwnAccountRowTests(TestCase): self.assertEqual(rows(), [("User bob: change email", True), ("User bob: change password", True)]) self.assertFalse(Logs.objects.filter(message__contains="pass-").exists()) + def test_a_wrong_old_password_is_a_failed_row(self): + # someone at an open session guessing the password + self.client.post(reverse("accounts:change_password"), { + "old_password": "guess-2026", "new_password1": "Bob-new-pass-1", "new_password2": "Bob-new-pass-1"}) + self.client.get(reverse("accounts:change_password")) + self.client.post(reverse("accounts:change_password"), { # none given: not a guess + "new_password1": "Bob-new-pass-1", "new_password2": "Bob-new-pass-1"}) + self.assertEqual(rows(), [("User bob: change password: wrong old password", False)]) + self.assertFalse(Logs.objects.filter(message__contains="pass-").exists()) + def test_the_first_login_password_change(self): self.bob.user_permissions.clear() UserAttributes.objects.update_or_create(user=self.bob, defaults={"must_change_password": True}) diff --git a/accounts/tests/test_email_otp.py b/accounts/tests/test_email_otp.py index 80d8e7ce..789a80df 100644 --- a/accounts/tests/test_email_otp.py +++ b/accounts/tests/test_email_otp.py @@ -10,6 +10,8 @@ from django.core.cache import cache from django.test import TestCase, override_settings from django.urls import reverse +from logs.models import Logs + URL = reverse("accounts:email_otp") @@ -35,6 +37,9 @@ class EmailOTPTests(TestCase): self.enroll() self.post("OTP.User@Example.com") # addresses match case-insensitively self.assertEqual([m.to for m in mail.outbox], [["otp.user@example.com"]]) + # the requester is anonymous: the row does not make it the user + self.assertEqual(list(Logs.objects.values_list("user", "message", "success", "ip")), + [("", "User otp-user: mail OTP QR code (lost OTP)", True, "127.0.0.1")]) def test_an_unenrolled_user_gets_no_mail_and_no_device(self): self.post("otp.user@example.com") @@ -63,6 +68,7 @@ class EmailOTPTests(TestCase): self.enroll() with self.assertLogs("accounts.views", level="ERROR"): self.post("otp.user@example.com") + self.assertIs(Logs.objects.get(message__startswith="User otp-user: mail OTP QR code (lost OTP):").success, False) @override_settings(OTP_ENABLED=False) diff --git a/accounts/views.py b/accounts/views.py index 817ce360..5fadd73c 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -222,6 +222,8 @@ def change_password(request): logging.getLogger(__name__).warning("Could not remove the generated admin password file") messages.success(request, _("Password Changed")) return redirect("accounts:profile") + if form.has_error("old_password", "password_incorrect"): # someone at an open session guessing it + failed(request, None, f"User {request.user.username}", "change password", "wrong old password") return render(request, "accounts/change_password_form.html", {"form": form, "must_change": must_change}) @@ -329,7 +331,9 @@ def email_otp(request): device = get_existing_totp_device(user) if device is not None: try: - send_email_with_otp(request, user) + # the row's user is the requester: anonymous + with audit(request, None, f"User {user.username}", "mail OTP QR code (lost OTP)"): + send_email_with_otp(request, user) except Exception: logging.getLogger(__name__).exception("Could not mail the OTP QR code to user %s", user.pk)